@oneuptime/common 14.0.8 → 14.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/KubernetesAiAgent.ts +364 -0
- package/Models/DatabaseModels/KubernetesCluster.ts +3 -3
- package/Models/DatabaseModels/Project.ts +55 -5
- package/Models/DatabaseModels/ProxmoxResource.ts +49 -1
- package/Models/DatabaseModels/RunnerJob.ts +75 -0
- package/Models/DatabaseModels/TeamComplianceSetting.ts +143 -2
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +17 -0
- package/Server/API/AIChatAPI.ts +6 -3
- package/Server/API/AIInvestigationAPI.ts +17 -10
- package/Server/API/AutoRemediationAPI.ts +207 -25
- package/Server/API/KubernetesClusterAiAccessAPI.ts +623 -21
- package/Server/API/MicrosoftTeamsAPI.ts +93 -14
- package/Server/API/ProxmoxResourceAPI.ts +244 -14
- package/Server/Infrastructure/GlobalCache.ts +32 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.ts +99 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.ts +137 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.ts +42 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Middleware/TelemetryIngest.ts +41 -18
- package/Server/Services/AIService.ts +70 -1
- package/Server/Services/AlertService.ts +352 -11
- package/Server/Services/AlertSeverityService.ts +65 -15
- package/Server/Services/AutoRemediationRuleService.ts +1 -1
- package/Server/Services/IncidentService.ts +280 -104
- package/Server/Services/IncidentSeverityService.ts +66 -15
- package/Server/Services/KubernetesAiAgentJobService.ts +185 -0
- package/Server/Services/KubernetesAiAgentService.ts +1634 -0
- package/Server/Services/KubernetesClusterAiAccessService.ts +1118 -451
- package/Server/Services/KubernetesClusterService.ts +129 -21
- package/Server/Services/ProjectService.ts +32 -0
- package/Server/Services/ProxmoxResourceService.ts +322 -5
- package/Server/Services/RunnerJobService.ts +241 -104
- package/Server/Services/RunnerService.ts +1 -1
- package/Server/Services/ScheduledMaintenanceService.ts +737 -1
- package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +414 -56
- package/Server/Services/TeamComplianceSettingService.ts +1125 -9
- package/Server/Utils/AI/ClusterAccess/ClusterAccessContext.ts +67 -9
- package/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.ts +39 -10
- package/Server/Utils/AI/ClusterAccess/KubectlJobRunner.ts +14 -3
- package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +37 -15
- package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +67 -24
- package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +48 -5
- package/Server/Utils/AI/SRE/IncidentPostmortemRunner.ts +40 -8
- package/Server/Utils/AI/SRE/InvestigationEligibility.ts +13 -2
- package/Server/Utils/AI/SRE/InvestigationGrader.ts +1 -1
- package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +6 -1
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +133 -7
- package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +58 -4
- package/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.ts +447 -0
- package/Server/Utils/Telemetry/ProxmoxNativePush.ts +903 -0
- package/Server/Utils/Telemetry/TelemetryEntity.ts +87 -27
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +684 -47
- package/Tests/App/AdminDashboard/RowActionPlacementOverrides.test.tsx +278 -0
- package/Tests/App/Dashboard/AIChatChartWidgetZoom.test.tsx +738 -0
- package/Tests/App/Dashboard/AIFeaturesSettingsPointers.test.tsx +147 -0
- package/Tests/App/Dashboard/AIInsightsKubernetesClusterPage.test.tsx +1523 -0
- package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +463 -3
- package/Tests/App/Dashboard/AffectedResourcesPickerDropdownPlacement.test.tsx +958 -0
- package/Tests/App/Dashboard/AffectedResourcesPickerFocusLeave.test.tsx +595 -0
- package/Tests/App/Dashboard/AskAiAvailability.test.ts +20 -3
- package/Tests/App/Dashboard/AskAiDisabled.test.tsx +8 -3
- package/Tests/App/Dashboard/CephRateChartReload.test.tsx +1456 -0
- package/Tests/App/Dashboard/CephTimeRangeZoom.test.tsx +1229 -0
- package/Tests/App/Dashboard/ChartCardTimeRangeZoom.test.tsx +151 -0
- package/Tests/App/Dashboard/ChartCardZoomAffordances.test.tsx +257 -0
- package/Tests/App/Dashboard/ChartRefetchFrame.test.tsx +141 -0
- package/Tests/App/Dashboard/ChartZoomHarness.tsx +249 -0
- package/Tests/App/Dashboard/ChartZoomStandIn.tsx +131 -0
- package/Tests/App/Dashboard/CloudServerlessSlowLoads.test.tsx +628 -0
- package/Tests/App/Dashboard/CloudServerlessTimeRangeZoom.test.tsx +607 -0
- package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +91 -19
- package/Tests/App/Dashboard/CompanionMetricsTabZoom.test.tsx +632 -0
- package/Tests/App/Dashboard/ContainerChartZoomStandIn.tsx +195 -0
- package/Tests/App/Dashboard/ContainerHostOverviewSlowLoads.test.tsx +542 -0
- package/Tests/App/Dashboard/ContainerHostOverviewTimeRangeZoom.test.tsx +848 -0
- package/Tests/App/Dashboard/ContainerMetricsTimeRangeZoom.test.tsx +561 -0
- package/Tests/App/Dashboard/DashboardBoardWideZoomShell.test.tsx +1147 -0
- package/Tests/App/Dashboard/DashboardEditPermissions.test.tsx +3 -2
- package/Tests/App/Dashboard/DashboardHistogramWidgetDragCursor.test.tsx +399 -0
- package/Tests/App/Dashboard/DashboardHistogramWidgetZoom.test.tsx +985 -0
- package/Tests/App/Dashboard/DashboardHistogramWidgetZoomHint.test.tsx +803 -0
- package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +1 -0
- package/Tests/App/Dashboard/DashboardSettingsPreviewZoom.test.tsx +679 -0
- package/Tests/App/Dashboard/DashboardSloAndDataSourceChartZoom.test.tsx +881 -0
- package/Tests/App/Dashboard/DashboardTraceStatusWidgets.test.tsx +878 -0
- package/Tests/App/Dashboard/DatabaseMetricChartModalTimeRangeZoom.test.tsx +442 -0
- package/Tests/App/Dashboard/DatabaseOverviewTimeRangeZoom.test.tsx +831 -0
- package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +43 -0
- package/Tests/App/Dashboard/DockerSwarmInsightsTimeRangeZoom.test.tsx +524 -0
- package/Tests/App/Dashboard/DockerSwarmRateChartRefresh.test.tsx +259 -0
- package/Tests/App/Dashboard/EdgeDetailPanelHistory.test.tsx +763 -0
- package/Tests/App/Dashboard/EdgeDetailPanelTimeRangeZoom.test.tsx +547 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsCases.tsx +565 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsKathmandu.test.tsx +73 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsKolkata.test.tsx +74 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsNewYork.test.tsx +72 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsUtc.test.tsx +72 -0
- package/Tests/App/Dashboard/EmbeddedMetricCardRefresh.test.tsx +310 -0
- package/Tests/App/Dashboard/EmbeddedMetricCardTimeRangeZoom.test.tsx +536 -0
- package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +3 -1
- package/Tests/App/Dashboard/EscalationRuleRowActions.test.tsx +660 -0
- package/Tests/App/Dashboard/EventNotes.test.tsx +2 -2
- package/Tests/App/Dashboard/EventOverviewPages.test.tsx +587 -13
- package/Tests/App/Dashboard/EventOverviewSnapshotExplorerZoom.test.tsx +1139 -0
- package/Tests/App/Dashboard/EventOverviewSnapshotZoomAcrossTabs.test.tsx +1057 -0
- package/Tests/App/Dashboard/EventOverviewTelemetrySnapshotZoom.test.tsx +757 -0
- package/Tests/App/Dashboard/ExceptionOccurrenceTrendZoom.test.tsx +675 -0
- package/Tests/App/Dashboard/ExceptionOccurrencesZoomSurvivesViewSwitch.test.tsx +405 -0
- package/Tests/App/Dashboard/ExceptionSignalPages.test.tsx +67 -3
- package/Tests/App/Dashboard/ExceptionTablesSpanStatus.test.tsx +515 -0
- package/Tests/App/Dashboard/ExplorerOwnZoomRegression.test.tsx +739 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisCases.tsx +461 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisKathmandu.test.tsx +25 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisKolkata.test.tsx +25 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisNewYork.test.tsx +25 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisUtc.test.tsx +24 -0
- package/Tests/App/Dashboard/FlowTopTalkersZoomRefetch.test.tsx +722 -0
- package/Tests/App/Dashboard/FlowViewportGuard.test.tsx +2526 -0
- package/Tests/App/Dashboard/HostPagesSlowLoads.test.tsx +872 -0
- package/Tests/App/Dashboard/HostPagesTimeRangeZoom.test.tsx +1208 -0
- package/Tests/App/Dashboard/IncidentRootCauseTimeRangeZoom.test.tsx +358 -0
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +100 -9
- package/Tests/App/Dashboard/InfrastructureMetricsTabTimeRangeZoom.test.tsx +485 -0
- package/Tests/App/Dashboard/InvestigationDrawerCompanionZoom.test.tsx +655 -0
- package/Tests/App/Dashboard/InvestigationDrawerZoom.test.tsx +830 -0
- package/Tests/App/Dashboard/InvestigationNotStarted.test.tsx +338 -2
- package/Tests/App/Dashboard/InvoicesPayInvoice.test.tsx +454 -16
- package/Tests/App/Dashboard/KubernetesClusterAiAccessSettings.test.ts +799 -819
- package/Tests/App/Dashboard/KubernetesClusterAiAgentOverviewCard.test.tsx +367 -0
- package/Tests/App/Dashboard/KubernetesClusterAiAgentStatus.test.ts +1202 -0
- package/Tests/App/Dashboard/KubernetesClusterAiDocumentationMarkdown.test.ts +106 -0
- package/Tests/App/Dashboard/KubernetesClusterAiNavigation.test.tsx +354 -0
- package/Tests/App/Dashboard/KubernetesClusterAiPage.test.tsx +1884 -2054
- package/Tests/App/Dashboard/KubernetesClusterAiPageServerParity.test.ts +478 -179
- package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +50 -0
- package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +2 -1
- package/Tests/App/Dashboard/KubernetesClusterOverviewZoom.test.tsx +1217 -0
- package/Tests/App/Dashboard/KubernetesClusterPagesZoom.test.tsx +731 -0
- package/Tests/App/Dashboard/KubernetesCostsLoadRecovery.test.tsx +869 -0
- package/Tests/App/Dashboard/KubernetesCostsZoom.test.tsx +1214 -0
- package/Tests/App/Dashboard/KubernetesCostsZoomExactHours.test.tsx +692 -0
- package/Tests/App/Dashboard/KubernetesNetworkThroughputReload.test.tsx +902 -0
- package/Tests/App/Dashboard/KubernetesNetworkZoomBucketEdges.test.tsx +571 -0
- package/Tests/App/Dashboard/KubernetesOverviewZoomBucketEdges.test.tsx +697 -0
- package/Tests/App/Dashboard/KubernetesResourceMetricsTabZoom.test.tsx +633 -0
- package/Tests/App/Dashboard/LogsExplorerTimeRangeZoom.test.tsx +470 -0
- package/Tests/App/Dashboard/LogsInsightsTimeRangeZoom.test.tsx +948 -0
- package/Tests/App/Dashboard/LogsViewerPinnedWindowZoom.test.tsx +486 -0
- package/Tests/App/Dashboard/MetricChartsBucketInspectorDoubleClick.test.tsx +1001 -0
- package/Tests/App/Dashboard/MetricExplorerInvestigationAutoRefresh.test.tsx +514 -0
- package/Tests/App/Dashboard/MetricExplorerSavedViewZoomRoundTrip.test.tsx +581 -0
- package/Tests/App/Dashboard/MetricExplorerTimeRangeZoom.test.tsx +1162 -0
- package/Tests/App/Dashboard/MetricMonitorPreviewZoomLabel.test.ts +72 -0
- package/Tests/App/Dashboard/MetricRowSparklineTimeRangeZoom.test.tsx +204 -0
- package/Tests/App/Dashboard/MetricViewFirstResultLands.test.tsx +408 -0
- package/Tests/App/Dashboard/MetricViewOwnZoomResetPlacement.test.tsx +952 -0
- package/Tests/App/Dashboard/MetricViewTimeRange.test.ts +139 -0
- package/Tests/App/Dashboard/MetricViewTimeRangeZoom.test.tsx +556 -0
- package/Tests/App/Dashboard/MonitorMetricsChartWrappersZoom.test.tsx +552 -0
- package/Tests/App/Dashboard/MonitorMetricsTabsTimeRangeZoom.test.tsx +740 -0
- package/Tests/App/Dashboard/MonitorOverviewChartsTimeRangeZoom.test.tsx +735 -0
- package/Tests/App/Dashboard/MonitorStepPreviewsTimeRangeZoom.test.tsx +835 -0
- package/Tests/App/Dashboard/NetworkDeviceTimeRangeZoom.test.tsx +1115 -0
- package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +186 -17
- package/Tests/App/Dashboard/OnCallRulesDeleteGuard.test.tsx +155 -46
- package/Tests/App/Dashboard/PasskeySettings.test.tsx +87 -2
- package/Tests/App/Dashboard/ProjectAISettingsPages.test.tsx +884 -0
- package/Tests/App/Dashboard/ProxmoxNodeRemove.test.tsx +621 -0
- package/Tests/App/Dashboard/ProxmoxOverviewSlowLoads.test.tsx +584 -0
- package/Tests/App/Dashboard/ProxmoxPagesTimeRangeZoom.test.tsx +823 -0
- package/Tests/App/Dashboard/ProxmoxRateChartReload.test.tsx +1253 -0
- package/Tests/App/Dashboard/PublicDashboardBoardWideZoom.test.tsx +605 -0
- package/Tests/App/Dashboard/RealRowsLineChart.tsx +250 -0
- package/Tests/App/Dashboard/RechartsHostsPlotCursor.test.tsx +513 -0
- package/Tests/App/Dashboard/RowActionPlacementOverrides.test.tsx +865 -0
- package/Tests/App/Dashboard/RumOverviewTimeRangeZoom.test.tsx +877 -0
- package/Tests/App/Dashboard/RunnerKubernetesAgentForm.test.tsx +20 -4
- package/Tests/App/Dashboard/RunnerSupersededByAiAgentNote.test.tsx +296 -0
- package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +146 -36
- package/Tests/App/Dashboard/ServiceDatabaseSlowLoads.test.tsx +1166 -0
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +1243 -104
- package/Tests/App/Dashboard/ServiceOverviewTimeRangeZoom.test.tsx +792 -0
- package/Tests/App/Dashboard/SloBudgetBurnDownZoom.test.tsx +771 -0
- package/Tests/App/Dashboard/SloBudgetBurnDownZoomAffordances.test.tsx +380 -0
- package/Tests/App/Dashboard/SloEventMetricsBarChartZoom.test.tsx +369 -0
- package/Tests/App/Dashboard/SloHistoryChartsTimeRangeZoom.test.tsx +694 -0
- package/Tests/App/Dashboard/SloHistoryChartsZoomAffordances.test.tsx +497 -0
- package/Tests/App/Dashboard/SloMetricsTimeRangeZoom.test.tsx +541 -0
- package/Tests/App/Dashboard/SlowLoadHarness.ts +123 -0
- package/Tests/App/Dashboard/SnapshotExplorerPrimaryZoom.test.tsx +1194 -0
- package/Tests/App/Dashboard/SnapshotViewerPinnedWindow.test.tsx +620 -0
- package/Tests/App/Dashboard/TelemetrySnapshotPanelTimeRangeZoom.test.tsx +765 -0
- package/Tests/App/Dashboard/TelemetrySnapshotZoom.test.tsx +516 -0
- package/Tests/App/Dashboard/TelemetrySnapshotZoomAcrossTabs.test.tsx +827 -0
- package/Tests/App/Dashboard/TimeRangeZoomPageHarness.tsx +463 -0
- package/Tests/App/Dashboard/TraceChartEditorStatusSplit.test.tsx +543 -0
- package/Tests/App/Dashboard/TraceStatusPresentation.test.tsx +967 -0
- package/Tests/App/Dashboard/TraceTimelineMinimapZoom.test.tsx +315 -0
- package/Tests/App/Dashboard/TracesAnalyticsStatusSplit.test.tsx +778 -0
- package/Tests/App/Dashboard/TracesAnalyticsViewZoom.test.tsx +349 -0
- package/Tests/App/Dashboard/TracesExplorerTimeRangeZoom.test.tsx +551 -0
- package/Tests/App/Dashboard/TracesStatusChartAndFacet.test.tsx +687 -0
- package/Tests/App/Dashboard/TracesViewerStaleResponses.test.tsx +458 -0
- package/Tests/App/Dashboard/UptimeStripsExcludedFromZoom.test.tsx +406 -0
- package/Tests/App/Dashboard/VMwareOverviewSlowLoads.test.tsx +535 -0
- package/Tests/App/Dashboard/VMwarePagesTimeRangeZoom.test.tsx +606 -0
- package/Tests/App/Dashboard/ValueWidgetSparklineStaleHover.test.tsx +1292 -0
- package/Tests/App/Dashboard/ValueWidgetSparklineZoom.test.tsx +765 -0
- package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +98 -0
- package/Tests/Models/DatabaseModels/KubernetesAiAgentModel.test.ts +295 -0
- package/Tests/Models/DatabaseModels/TeamComplianceSettingModel.test.ts +347 -0
- package/Tests/Server/API/AIChatRouteAuthorization.test.ts +58 -0
- package/Tests/Server/API/AIGenerationProjectAIToggle.test.ts +1 -1
- package/Tests/Server/API/AIInvestigationAPI.test.ts +123 -45
- package/Tests/Server/API/AutoRemediationApproveClusterRound.test.ts +1329 -0
- package/Tests/Server/API/AutoRemediationApproveKubectlRecheck.test.ts +25 -2
- package/Tests/Server/API/KubernetesClusterAiAccessAPI.test.ts +2 -0
- package/Tests/Server/API/KubernetesClusterAiAccessAPIAgent.test.ts +903 -0
- package/Tests/Server/API/MicrosoftTeamsChatsRefreshRoute.test.ts +667 -0
- package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +294 -6
- package/Tests/Server/API/ProxmoxResourceAPIRemoveNode.test.ts +1932 -0
- package/Tests/Server/Infrastructure/GlobalCache.test.ts +195 -0
- package/Tests/Server/Infrastructure/Postgres/AddKubernetesAiAgentAndAiDefaultsMigration.test.ts +623 -0
- package/Tests/Server/Infrastructure/Postgres/AddKubernetesClusterAiAccessMigration.test.ts +87 -10
- package/Tests/Server/Infrastructure/Postgres/AddTeamComplianceRuleScopeMigration.test.ts +622 -0
- package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +61 -4
- package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +98 -2
- package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +105 -2
- package/Tests/Server/Services/AIServiceBalanceBlocker.test.ts +282 -0
- package/Tests/Server/Services/AlertMonitorClearWritesNull.test.ts +409 -0
- package/Tests/Server/Services/AlertMonitorEditFeed.test.ts +474 -0
- package/Tests/Server/Services/AlertMonitorEditGuard.test.ts +610 -0
- package/Tests/Server/Services/AutoRemediationClusterRuleEngine.test.ts +2 -0
- package/Tests/Server/Services/AutoRemediationRuleCommandRunners.test.ts +2 -1
- package/Tests/Server/Services/IncidentAffectedMonitorsEdit.test.ts +1360 -0
- package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +3 -1
- package/Tests/Server/Services/IncidentSeverityDeletePausesComplianceRules.test.ts +488 -0
- package/Tests/Server/Services/KubernetesAiAgentJobService.test.ts +329 -0
- package/Tests/Server/Services/KubernetesAiAgentServiceHelpers.test.ts +618 -0
- package/Tests/Server/Services/KubernetesAiAgentServiceLifecycle.test.ts +1444 -0
- package/Tests/Server/Services/KubernetesAiAgentServiceRegister.test.ts +1666 -0
- package/Tests/Server/Services/KubernetesClusterAiAccessDeletedRunnerRemedy.test.ts +140 -111
- package/Tests/Server/Services/KubernetesClusterAiAccessGapNextSteps.test.ts +267 -105
- package/Tests/Server/Services/KubernetesClusterAiAccessLegacySuperseded.test.ts +440 -0
- package/Tests/Server/Services/KubernetesClusterAiAccessService.test.ts +567 -411
- package/Tests/Server/Services/KubernetesClusterAiAccessTarget.test.ts +1168 -0
- package/Tests/Server/Services/KubernetesClusterAiSettingsPermission.test.ts +410 -12
- package/Tests/Server/Services/KubernetesClusterServiceAiAccessFeed.test.ts +55 -0
- package/Tests/Server/Services/ProjectServiceNewProjectAiDefaults.test.ts +288 -0
- package/Tests/Server/Services/ProxmoxResourcePostgres.test.ts +5633 -0
- package/Tests/Server/Services/ProxmoxResourceService.test.ts +6458 -5
- package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +48 -15
- package/Tests/Server/Services/RunnerJobEnqueueKubectlAiAgent.test.ts +620 -0
- package/Tests/Server/Services/RunnerJobKubectlWriteScope.test.ts +170 -3
- package/Tests/Server/Services/RunnerJobTimeoutReason.test.ts +72 -0
- package/Tests/Server/Services/RunnerServiceAgentName.test.ts +6 -4
- package/Tests/Server/Services/ScheduledMaintenanceAttachmentEdit.test.ts +2113 -0
- package/Tests/Server/Services/ScheduledMaintenanceMonitorRelease.test.ts +972 -0
- package/Tests/Server/Services/TeamComplianceSettingPostgres.test.ts +2237 -0
- package/Tests/Server/Services/TeamComplianceSettingService.test.ts +3988 -138
- package/Tests/Server/Utils/AI/ClusterAccessContext.test.ts +107 -1
- package/Tests/Server/Utils/AI/IncidentPostmortemRunner.test.ts +351 -0
- package/Tests/Server/Utils/AI/InvestigationClusterAccessWiring.test.ts +2 -0
- package/Tests/Server/Utils/AI/InvestigationEligibility.test.ts +310 -0
- package/Tests/Server/Utils/AI/KubectlInvestigationToolkit.test.ts +70 -0
- package/Tests/Server/Utils/AI/KubectlJobRunnerFinishedJob.test.ts +58 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsKubectl.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsKubectlOutcome.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsLiveCluster.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsRunnerScope.test.ts +125 -1
- package/Tests/Server/Utils/AI/RemediationExecutionRunner.test.ts +252 -3
- package/Tests/Server/Utils/AI/RemediationExecutionRunnerClusterMode.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationExecutionRunnerProjectGates.test.ts +240 -0
- package/Tests/Server/Utils/AI/RemediationKubectlCopyParity.test.ts +2 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutor.test.ts +2 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorUnknownRun.test.ts +2 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanVerification.test.ts +2 -0
- package/Tests/Server/Utils/Telemetry/HostAssetAttributesFromOtlp.test.ts +68 -0
- package/Tests/Server/Utils/Telemetry/InventoryMirrorPostgres.test.ts +370 -0
- package/Tests/Server/Utils/Telemetry/NetworkDeviceInventoryAttributes.test.ts +522 -0
- package/Tests/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.test.ts +155 -0
- package/Tests/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.test.ts +8631 -0
- package/Tests/Server/Utils/Telemetry/ProxmoxNativePush.test.ts +2852 -0
- package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +367 -1
- package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +45 -3
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +25 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNames.test.ts +3119 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNamesPostgres.test.ts +1721 -0
- package/Tests/Types/Kubernetes/KubernetesAgentChartAiAccess.test.ts +581 -113
- package/Tests/Types/Kubernetes/KubernetesAiAgentAccessContract.test.ts +510 -0
- package/Tests/Types/Team/ComplianceRule.test.ts +1029 -0
- package/Tests/Types/Team/TeamComplianceStatus.test.ts +176 -0
- package/Tests/UI/Components/ActionButton/RowActions.test.tsx +641 -0
- package/Tests/UI/Components/ActionButton/RowActionsAccessibility.test.tsx +414 -0
- package/Tests/UI/Components/ActionButton/SplitActionButtons.test.ts +565 -0
- package/Tests/UI/Components/Charts/BarChartDragToSelect.test.tsx +386 -0
- package/Tests/UI/Components/Charts/ChartDoubleClickResetWithoutDblclick.test.tsx +1025 -0
- package/Tests/UI/Components/Charts/ChartGroupPageZoomHint.test.tsx +196 -0
- package/Tests/UI/Components/Charts/ChartPlaceholdersTakeDoubleClick.test.tsx +1861 -0
- package/Tests/UI/Components/Charts/ChartPlotClicksWhileResetArmed.test.tsx +382 -0
- package/Tests/UI/Components/Charts/ChartRangeSelection.test.tsx +704 -0
- package/Tests/UI/Components/Charts/ChartRangeSelectionDoubleClickReset.test.tsx +1481 -0
- package/Tests/UI/Components/Charts/ChartRangeSelectionRealRecharts.test.tsx +918 -0
- package/Tests/UI/Components/Charts/ChartRowBucketStart.test.ts +350 -0
- package/Tests/UI/Components/Charts/DoubleClickReset.test.tsx +996 -0
- package/Tests/UI/Components/Charts/HistogramDoubleClickResetRealRecharts.test.tsx +965 -0
- package/Tests/UI/Components/Charts/HistogramDriftThenRefetch.test.tsx +277 -0
- package/Tests/UI/Components/Charts/HistogramRangeSelectionDataChange.test.tsx +1757 -0
- package/Tests/UI/Components/Charts/HistogramRangeSelectionRealRecharts.test.tsx +560 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/ChartPageTimeRangeZoom.test.tsx +513 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.test.tsx +845 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.test.tsx +119 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHintTailwind.test.tsx +274 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.test.ts +433 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.test.tsx +863 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/ZoomDocsPromises.test.tsx +1147 -0
- package/Tests/UI/Components/DisabledButtonTooltip.test.tsx +14 -3
- package/Tests/UI/Components/EntityDropdownEscape.test.tsx +454 -0
- package/Tests/UI/Components/EntityDropdownFocusLeave.test.tsx +420 -0
- package/Tests/UI/Components/Forms/FormFieldDropdownDisabled.test.tsx +233 -0
- package/Tests/UI/Components/Input.test.tsx +80 -1
- package/Tests/UI/Components/LogsAnalyticsViewZoom.test.tsx +947 -0
- package/Tests/UI/Components/LogsHistogramDragTooltip.test.tsx +17 -5
- package/Tests/UI/Components/LogsViewerAnalyticsZoom.test.tsx +501 -0
- package/Tests/UI/Components/LogsViewerHistogramZoom.test.tsx +1 -1
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +6 -4
- package/Tests/UI/Components/ModelTable/BaseModelTablePermissionGating.test.tsx +60 -8
- package/Tests/UI/Components/ModelTable/BaseModelTableRowActions.test.tsx +476 -0
- package/Tests/UI/Components/ModelTable/ModelTableWrapContent.test.tsx +7 -5
- package/Tests/UI/Components/MoreMenuPortal.test.tsx +623 -0
- package/Tests/UI/Components/StatusPage/ResourceGridActions.test.tsx +538 -0
- package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +14 -4
- package/Tests/UI/Components/TableCellWrapping.test.tsx +3 -3
- package/Tests/UI/Components/TableRowActions.test.tsx +472 -0
- package/Tests/UI/Components/TelemetryHistogramDragTooltip.test.tsx +1 -1
- package/Tests/UI/Components/TelemetryViewer/TelemetryViewerHistogramClickZoom.test.tsx +1 -1
- package/Tests/UI/Components/TelemetryViewer/TelemetryViewerZoomScope.test.tsx +470 -0
- package/Tests/UI/Components/TelemetryViewer/UseViewerTimeRangeZoom.test.tsx +571 -0
- package/Tests/UI/Components/TelemetryViewer/ViewerDoubleClickResetUnderDataChange.test.tsx +928 -0
- package/Tests/UI/Components/TelemetryViewer/ViewerFollowsEnclosingZoom.test.tsx +1569 -0
- package/Tests/UI/Components/Toggle.test.tsx +66 -0
- package/Tests/UI/MicrosoftTeams/MicrosoftTeamsChatsCardRefreshNames.test.tsx +1023 -0
- package/Tests/UI/Rum/ChunkLoader.test.ts +201 -0
- package/Tests/UI/Rum/FidelityNotices.test.ts +54 -0
- package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +72 -0
- package/Tests/UI/Rum/ReplayFrameCapture.test.ts +1009 -9
- package/Tests/UI/Rum/ReplayRecordedAssets.test.ts +1069 -0
- package/Tests/UI/Rum/ReplayStage.test.tsx +913 -2
- package/Tests/UI/Rum/SessionReplayTable.test.tsx +443 -8
- package/Tests/UI/Rum/SessionReplayUsersTable.test.tsx +439 -9
- package/Tests/UI/Rum/UserFlowTablesRowActions.test.tsx +501 -0
- package/Tests/Utils/AiRemediation/KubectlPolicyChartRbacParity.test.ts +47 -19
- package/Tests/Utils/AiRemediation/KubectlWriteScopeCallerParity.test.ts +2 -0
- package/Tests/Utils/AiRemediation/KubernetesAiAgentPolicyCopyParity.test.ts +396 -0
- package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +104 -1
- package/Types/AI/AIRunType.ts +9 -4
- package/Types/AI/InvestigationNotStartedReason.ts +6 -0
- package/Types/AutoRemediation/AiRemediationCommandPlan.ts +14 -16
- package/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.ts +28 -0
- package/Types/Dashboard/DashboardComponents/DashboardTraceChartComponent.ts +4 -2
- package/Types/Kubernetes/KubernetesClusterAiAccess.ts +263 -32
- package/Types/Kubernetes/KubernetesClusterAiAccessPermissions.ts +20 -16
- package/Types/Kubernetes/KubernetesClusterAiInsights.ts +81 -0
- package/Types/Monitor/ProxmoxAlertTemplates.ts +4 -4
- package/Types/Team/ComplianceNotificationChannel.ts +22 -0
- package/Types/Team/ComplianceRule.ts +434 -0
- package/Types/Team/ComplianceRuleType.ts +21 -0
- package/Types/Team/TeamComplianceStatus.ts +81 -0
- package/Types/Telemetry/TelemetryIngestSurface.ts +28 -0
- package/UI/Components/ActionButton/ActionButtonSchema.ts +21 -0
- package/UI/Components/ActionButton/RowActions.tsx +238 -0
- package/UI/Components/ActionButton/SplitActionButtons.ts +173 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +32 -2
- package/UI/Components/Charts/Bar/BarChart.tsx +40 -3
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +37 -11
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +80 -243
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +122 -31
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +95 -244
- package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +214 -0
- package/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.ts +32 -0
- package/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.ts +599 -0
- package/UI/Components/Charts/Line/LineChart.tsx +32 -2
- package/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.tsx +70 -0
- package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.tsx +226 -0
- package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.tsx +73 -0
- package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.ts +163 -0
- package/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.ts +215 -0
- package/UI/Components/Charts/Utils/DataPoint.ts +20 -8
- package/UI/Components/Charts/Utils/XAxis.ts +75 -0
- package/UI/Components/Charts/Utils/useHistogramRangeSelection.ts +493 -59
- package/UI/Components/EntityDropdown/EntityDropdown.tsx +107 -11
- package/UI/Components/Forms/Fields/FormField.tsx +10 -0
- package/UI/Components/Input/Input.tsx +90 -57
- package/UI/Components/List/ListRow.tsx +8 -74
- package/UI/Components/LogsViewer/LogsViewer.tsx +40 -11
- package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +22 -6
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +209 -6
- package/UI/Components/LogsViewer/components/LogsHistogram.tsx +127 -84
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +3 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +12 -1
- package/UI/Components/MoreMenu/MoreMenu.tsx +365 -30
- package/UI/Components/MoreMenu/MoreMenuItem.tsx +70 -17
- package/UI/Components/OrderedStatesList/Item.tsx +7 -72
- package/UI/Components/StatusPage/ResourceGrid.tsx +126 -77
- package/UI/Components/Table/TableBody.tsx +8 -2
- package/UI/Components/Table/TableRow.tsx +20 -140
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +41 -10
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +47 -9
- package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +25 -6
- package/UI/Components/TelemetryViewer/types.ts +2 -0
- package/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.ts +448 -0
- package/UI/Components/Toggle/Toggle.tsx +26 -0
- package/UI/Components/Tooltip/Tooltip.tsx +8 -1
- package/UI/Styles/Theme.css +14 -2
- package/Utils/AiRemediation/CommandPolicy.ts +1 -1
- package/Utils/AiRemediation/KubectlPolicy.ts +1 -1
- package/Utils/Dashboard/DashboardTimeRangeZoom.ts +46 -2
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js +385 -0
- package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js.map +1 -0
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js +3 -3
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +56 -5
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ProxmoxResource.js +51 -1
- package/build/dist/Models/DatabaseModels/ProxmoxResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RunnerJob.js +74 -0
- package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js +134 -3
- package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/AIChatAPI.js +6 -1
- package/build/dist/Server/API/AIChatAPI.js.map +1 -1
- package/build/dist/Server/API/AIInvestigationAPI.js +17 -10
- package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
- package/build/dist/Server/API/AutoRemediationAPI.js +144 -24
- package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
- package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js +436 -22
- package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +69 -13
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/ProxmoxResourceAPI.js +187 -14
- package/build/dist/Server/API/ProxmoxResourceAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/GlobalCache.js +27 -0
- package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js +50 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js +84 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js +31 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/TelemetryIngest.js +34 -19
- package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
- package/build/dist/Server/Services/AIService.js +59 -1
- package/build/dist/Server/Services/AIService.js.map +1 -1
- package/build/dist/Server/Services/AlertService.js +248 -13
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/AlertSeverityService.js +51 -15
- package/build/dist/Server/Services/AlertSeverityService.js.map +1 -1
- package/build/dist/Server/Services/AutoRemediationRuleService.js +1 -1
- package/build/dist/Server/Services/AutoRemediationRuleService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +199 -72
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentSeverityService.js +51 -15
- package/build/dist/Server/Services/IncidentSeverityService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesAiAgentJobService.js +158 -0
- package/build/dist/Server/Services/KubernetesAiAgentJobService.js.map +1 -0
- package/build/dist/Server/Services/KubernetesAiAgentService.js +1145 -0
- package/build/dist/Server/Services/KubernetesAiAgentService.js.map +1 -0
- package/build/dist/Server/Services/KubernetesClusterAiAccessService.js +842 -371
- package/build/dist/Server/Services/KubernetesClusterAiAccessService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +86 -17
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +22 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxResourceService.js +258 -3
- package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
- package/build/dist/Server/Services/RunnerJobService.js +178 -92
- package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
- package/build/dist/Server/Services/RunnerService.js +1 -1
- package/build/dist/Server/Services/RunnerService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +535 -3
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +316 -46
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/TeamComplianceSettingService.js +833 -7
- package/build/dist/Server/Services/TeamComplianceSettingService.js.map +1 -1
- package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js +52 -9
- package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js.map +1 -1
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js +13 -9
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js.map +1 -1
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js +7 -3
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +29 -15
- package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +56 -20
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +39 -5
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js +30 -3
- package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js +13 -2
- package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/InvestigationGrader.js +1 -1
- package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +4 -2
- package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +111 -6
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +40 -4
- package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js +296 -0
- package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js +699 -0
- package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +77 -19
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +505 -42
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/AI/AIRunType.js +9 -4
- package/build/dist/Types/AI/AIRunType.js.map +1 -1
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +10 -16
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js +28 -0
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js.map +1 -0
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js +72 -16
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js.map +1 -1
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js +20 -16
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js.map +1 -1
- package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js +17 -0
- package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js.map +1 -0
- package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +4 -4
- package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js.map +1 -1
- package/build/dist/Types/Team/ComplianceNotificationChannel.js +23 -0
- package/build/dist/Types/Team/ComplianceNotificationChannel.js.map +1 -0
- package/build/dist/Types/Team/ComplianceRule.js +304 -0
- package/build/dist/Types/Team/ComplianceRule.js.map +1 -0
- package/build/dist/Types/Team/ComplianceRuleType.js +21 -0
- package/build/dist/Types/Team/ComplianceRuleType.js.map +1 -1
- package/build/dist/Types/Team/TeamComplianceStatus.js +2 -0
- package/build/dist/Types/Team/TeamComplianceStatus.js.map +1 -0
- package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +25 -0
- package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
- package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js +20 -1
- package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js.map +1 -1
- package/build/dist/UI/Components/ActionButton/RowActions.js +113 -0
- package/build/dist/UI/Components/ActionButton/RowActions.js.map +1 -0
- package/build/dist/UI/Components/ActionButton/SplitActionButtons.js +71 -0
- package/build/dist/UI/Components/ActionButton/SplitActionButtons.js.map +1 -0
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +16 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +16 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +27 -5
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +61 -194
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +84 -26
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +75 -196
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +133 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js +26 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js.map +1 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js +371 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js.map +1 -0
- package/build/dist/UI/Components/Charts/Line/LineChart.js +16 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js +35 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js.map +1 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js +107 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js.map +1 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js +41 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js.map +1 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js +117 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js.map +1 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js +121 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js.map +1 -0
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js +15 -7
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/XAxis.js +70 -0
- package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js +305 -38
- package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js.map +1 -1
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +89 -14
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +11 -2
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +69 -54
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/List/ListRow.js +4 -39
- package/build/dist/UI/Components/List/ListRow.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +31 -8
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +10 -1
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +139 -21
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +30 -8
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +10 -0
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenu.js +253 -14
- package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js +43 -13
- package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js.map +1 -1
- package/build/dist/UI/Components/OrderedStatesList/Item.js +2 -38
- package/build/dist/UI/Components/OrderedStatesList/Item.js.map +1 -1
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js +53 -15
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
- package/build/dist/UI/Components/Table/TableBody.js +6 -2
- package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
- package/build/dist/UI/Components/Table/TableRow.js +12 -75
- package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +32 -7
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +27 -9
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +11 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js +272 -0
- package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js.map +1 -0
- package/build/dist/UI/Components/Toggle/Toggle.js +14 -1
- package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js +1 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
- package/build/dist/Utils/AiRemediation/CommandPolicy.js +1 -1
- package/build/dist/Utils/AiRemediation/CommandPolicy.js.map +1 -1
- package/build/dist/Utils/AiRemediation/KubectlPolicy.js +1 -1
- package/build/dist/Utils/AiRemediation/KubectlPolicy.js.map +1 -1
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +27 -1
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1666 @@
|
|
|
1
|
+
import KubernetesAiAgent from "../../../Models/DatabaseModels/KubernetesAiAgent";
|
|
2
|
+
import KubernetesCluster from "../../../Models/DatabaseModels/KubernetesCluster";
|
|
3
|
+
import Runner, {
|
|
4
|
+
RunnerConnectionStatus,
|
|
5
|
+
} from "../../../Models/DatabaseModels/Runner";
|
|
6
|
+
import KubernetesAiAgentService, {
|
|
7
|
+
KUBERNETES_AI_AGENT_REGISTRATION_RETRY_AFTER_SECONDS,
|
|
8
|
+
KUBERNETES_AI_AGENT_SELECT_WITHOUT_KEY,
|
|
9
|
+
KubernetesAiAgentRegistrationRefusedException,
|
|
10
|
+
KubernetesAiAgentRegistrationResult,
|
|
11
|
+
MAX_KUBERNETES_AI_AGENT_CLUSTER_NAME_LENGTH,
|
|
12
|
+
MAX_KUBERNETES_AI_AGENTS_PER_PROJECT,
|
|
13
|
+
MAX_NEW_KUBERNETES_AI_AGENTS_PER_PROJECT_PER_HOUR,
|
|
14
|
+
Service as KubernetesAiAgentServiceClass,
|
|
15
|
+
} from "../../../Server/Services/KubernetesAiAgentService";
|
|
16
|
+
import KubernetesClusterAiAccessService from "../../../Server/Services/KubernetesClusterAiAccessService";
|
|
17
|
+
import KubernetesClusterFeedService from "../../../Server/Services/KubernetesClusterFeedService";
|
|
18
|
+
import KubernetesClusterService from "../../../Server/Services/KubernetesClusterService";
|
|
19
|
+
import RunnerService from "../../../Server/Services/RunnerService";
|
|
20
|
+
import logger from "../../../Server/Utils/Logger";
|
|
21
|
+
import ColumnLength from "../../../Types/Database/ColumnLength";
|
|
22
|
+
import ForbiddenException from "../../../Types/Exception/ForbiddenException";
|
|
23
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
24
|
+
import {
|
|
25
|
+
KUBERNETES_AI_AGENT_ALIVE_WINDOW_IN_MINUTES,
|
|
26
|
+
KubernetesAgentPosture,
|
|
27
|
+
KubernetesAiAgentRegistrationRefusalReason,
|
|
28
|
+
KubernetesAiRemediationMode,
|
|
29
|
+
isTransientKubernetesAiAgentRegistrationRefusal,
|
|
30
|
+
} from "../../../Types/Kubernetes/KubernetesClusterAiAccess";
|
|
31
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
32
|
+
import PositiveNumber from "../../../Types/PositiveNumber";
|
|
33
|
+
import { afterEach, describe, expect, jest, test } from "@jest/globals";
|
|
34
|
+
import { FindOperator } from "typeorm";
|
|
35
|
+
|
|
36
|
+
/*
|
|
37
|
+
* KubernetesAiAgentService.register — how the Kubernetes AI agent gets its
|
|
38
|
+
* identity with nothing but the project's telemetry ingestion key and its
|
|
39
|
+
* cluster's name (DESIGN §5.2):
|
|
40
|
+
*
|
|
41
|
+
* 1. an empty or over-long cluster name is refused (cluster_name_invalid);
|
|
42
|
+
* 2. while the cluster's previous in-cluster Runner is online the agent is
|
|
43
|
+
* asked to wait (legacy_runner_online, transient, 20s) — an ingestion
|
|
44
|
+
* key alone never takes a cluster from a live Runner;
|
|
45
|
+
* 3. an existing agent row is re-keyed on proof of continuity, or when it
|
|
46
|
+
* is reset / signed off / offline; a live one is refused
|
|
47
|
+
* (previous_instance_online, transient, 20s) and the refusal recorded;
|
|
48
|
+
* 4. a new row is bounded per project (250) and per hour (30);
|
|
49
|
+
* 5. only the key's hash is stored, and the posture is stored with
|
|
50
|
+
* inCluster and the cluster's identifier forced;
|
|
51
|
+
* 6. a cluster nobody configured gets the first-connection defaults
|
|
52
|
+
* (investigation on; Ask for approval when the chart allows writes),
|
|
53
|
+
* never aiAccessConfiguredAt;
|
|
54
|
+
* 7. nothing is deleted or unbound, and one feed item marks the first
|
|
55
|
+
* connection.
|
|
56
|
+
*
|
|
57
|
+
* Everything below the service boundary is stubbed: no database.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/*
|
|
61
|
+
* Spy handles are held through this rather than a SpiedFunction/SpyInstance
|
|
62
|
+
* type (see UserProjectSsoConsentService.test.ts for why).
|
|
63
|
+
*/
|
|
64
|
+
interface SpyCalls {
|
|
65
|
+
mock: { calls: Array<Array<unknown>> };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
69
|
+
"11111111-1111-4111-8111-111111111111",
|
|
70
|
+
);
|
|
71
|
+
const CLUSTER_ID: ObjectID = new ObjectID(
|
|
72
|
+
"22222222-2222-4222-8222-222222222222",
|
|
73
|
+
);
|
|
74
|
+
const AGENT_ID: ObjectID = new ObjectID("33333333-3333-4333-8333-333333333333");
|
|
75
|
+
const LEGACY_RUNNER_ID: ObjectID = new ObjectID(
|
|
76
|
+
"44444444-4444-4444-8444-444444444444",
|
|
77
|
+
);
|
|
78
|
+
const INGESTION_KEY_ID: ObjectID = new ObjectID(
|
|
79
|
+
"55555555-5555-4555-8555-555555555555",
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const CURRENT_KEY: string = "ab".repeat(32);
|
|
83
|
+
const ALIVE_WINDOW_MS: number =
|
|
84
|
+
KUBERNETES_AI_AGENT_ALIVE_WINDOW_IN_MINUTES * 60 * 1000;
|
|
85
|
+
|
|
86
|
+
function msAgo(ms: number): Date {
|
|
87
|
+
return new Date(Date.now() - ms);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function makeCluster(
|
|
91
|
+
overrides: Partial<KubernetesCluster> = {},
|
|
92
|
+
): KubernetesCluster {
|
|
93
|
+
const cluster: KubernetesCluster = new KubernetesCluster();
|
|
94
|
+
cluster.id = CLUSTER_ID;
|
|
95
|
+
cluster.projectId = PROJECT_ID;
|
|
96
|
+
cluster.clusterIdentifier = "prod-us";
|
|
97
|
+
cluster.isAiInvestigationEnabled = true;
|
|
98
|
+
cluster.aiRemediationMode = KubernetesAiRemediationMode.Disabled;
|
|
99
|
+
Object.assign(cluster, overrides);
|
|
100
|
+
return cluster;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// An existing agent row as the registration lookup reads it (with its hash).
|
|
104
|
+
function makeAgent(
|
|
105
|
+
overrides: Partial<Record<string, unknown>> = {},
|
|
106
|
+
): KubernetesAiAgent {
|
|
107
|
+
const agent: KubernetesAiAgent = new KubernetesAiAgent();
|
|
108
|
+
agent.id = AGENT_ID;
|
|
109
|
+
agent.projectId = PROJECT_ID;
|
|
110
|
+
agent.kubernetesClusterId = CLUSTER_ID;
|
|
111
|
+
agent.keyHash = KubernetesAiAgentServiceClass.hashKey(CURRENT_KEY);
|
|
112
|
+
agent.connectionStatus = "connected";
|
|
113
|
+
agent.lastAliveAt = msAgo(20 * 1000);
|
|
114
|
+
agent.lastRegisteredAt = msAgo(60 * 60 * 1000);
|
|
115
|
+
agent.agentVersion = "14.1.0";
|
|
116
|
+
agent.posture = {
|
|
117
|
+
clusterIdentifier: "prod-us",
|
|
118
|
+
inCluster: true,
|
|
119
|
+
allowWrites: false,
|
|
120
|
+
};
|
|
121
|
+
Object.assign(agent, overrides);
|
|
122
|
+
return agent;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function makeLegacyRunner(): Runner {
|
|
126
|
+
const runner: Runner = new Runner();
|
|
127
|
+
runner.id = LEGACY_RUNNER_ID;
|
|
128
|
+
runner.name = "kubernetes-agent/prod-us";
|
|
129
|
+
runner.lastAlive = msAgo(10 * 1000);
|
|
130
|
+
runner.connectionStatus = RunnerConnectionStatus.Connected;
|
|
131
|
+
return runner;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface Harness {
|
|
135
|
+
findOrCreateCluster: SpyCalls;
|
|
136
|
+
clusterFindOneBy: SpyCalls;
|
|
137
|
+
clusterUpdateOneBy: SpyCalls;
|
|
138
|
+
clusterUpdateOneById: SpyCalls;
|
|
139
|
+
feed: SpyCalls;
|
|
140
|
+
legacyLookup: SpyCalls;
|
|
141
|
+
legacyOnline: SpyCalls;
|
|
142
|
+
agentFindOneBy: SpyCalls;
|
|
143
|
+
agentCountBy: SpyCalls;
|
|
144
|
+
agentCreate: SpyCalls;
|
|
145
|
+
agentUpdateOneById: SpyCalls;
|
|
146
|
+
agentUpdateColumns: SpyCalls;
|
|
147
|
+
runnerDeletes: Array<SpyCalls>;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface HarnessOptions {
|
|
151
|
+
cluster?: KubernetesCluster;
|
|
152
|
+
existing?: KubernetesAiAgent | null;
|
|
153
|
+
legacyRunner?: Runner | null;
|
|
154
|
+
legacyOnline?: boolean;
|
|
155
|
+
totalAgents?: number;
|
|
156
|
+
agentsInLastHour?: number;
|
|
157
|
+
clusterUpdateCount?: number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function setUp(options: HarnessOptions = {}): Harness {
|
|
161
|
+
const cluster: KubernetesCluster = options.cluster || makeCluster();
|
|
162
|
+
|
|
163
|
+
for (const level of ["info", "warn", "error", "debug"] as const) {
|
|
164
|
+
jest.spyOn(logger, level).mockImplementation((): void => {
|
|
165
|
+
return undefined;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const findOrCreateCluster: SpyCalls = jest
|
|
170
|
+
.spyOn(KubernetesClusterService, "findOrCreateByClusterIdentifier")
|
|
171
|
+
.mockResolvedValue(cluster) as unknown as SpyCalls;
|
|
172
|
+
|
|
173
|
+
const clusterFindOneBy: SpyCalls = jest
|
|
174
|
+
.spyOn(KubernetesClusterService, "findOneBy")
|
|
175
|
+
.mockResolvedValue(cluster) as unknown as SpyCalls;
|
|
176
|
+
|
|
177
|
+
const clusterUpdateOneBy: SpyCalls = jest
|
|
178
|
+
.spyOn(KubernetesClusterService, "updateOneBy")
|
|
179
|
+
.mockResolvedValue(options.clusterUpdateCount ?? 1) as unknown as SpyCalls;
|
|
180
|
+
|
|
181
|
+
const clusterUpdateOneById: SpyCalls = jest
|
|
182
|
+
.spyOn(KubernetesClusterService, "updateOneById")
|
|
183
|
+
.mockResolvedValue(1) as unknown as SpyCalls;
|
|
184
|
+
|
|
185
|
+
const feed: SpyCalls = jest
|
|
186
|
+
.spyOn(KubernetesClusterFeedService, "createKubernetesClusterFeedItem")
|
|
187
|
+
.mockResolvedValue(undefined) as unknown as SpyCalls;
|
|
188
|
+
|
|
189
|
+
const legacyLookup: SpyCalls = jest
|
|
190
|
+
.spyOn(KubernetesClusterAiAccessService, "getLegacyAgentRunnerForCluster")
|
|
191
|
+
.mockResolvedValue(
|
|
192
|
+
options.legacyRunner === undefined ? null : options.legacyRunner,
|
|
193
|
+
) as unknown as SpyCalls;
|
|
194
|
+
|
|
195
|
+
const legacyOnline: SpyCalls = jest
|
|
196
|
+
.spyOn(KubernetesClusterAiAccessService, "isRunnerOnline")
|
|
197
|
+
.mockReturnValue(options.legacyOnline === true) as unknown as SpyCalls;
|
|
198
|
+
|
|
199
|
+
const agentFindOneBy: SpyCalls = jest
|
|
200
|
+
.spyOn(KubernetesAiAgentService, "findOneBy")
|
|
201
|
+
.mockResolvedValue(
|
|
202
|
+
options.existing === undefined ? null : options.existing,
|
|
203
|
+
) as unknown as SpyCalls;
|
|
204
|
+
|
|
205
|
+
const agentCountBy: SpyCalls = jest
|
|
206
|
+
.spyOn(KubernetesAiAgentService, "countBy")
|
|
207
|
+
.mockImplementation(async (countBy: { query: Record<string, unknown> }) => {
|
|
208
|
+
return new PositiveNumber(
|
|
209
|
+
countBy.query["createdAt"] !== undefined
|
|
210
|
+
? options.agentsInLastHour || 0
|
|
211
|
+
: options.totalAgents || 0,
|
|
212
|
+
);
|
|
213
|
+
}) as unknown as SpyCalls;
|
|
214
|
+
|
|
215
|
+
const agentCreate: SpyCalls = jest
|
|
216
|
+
.spyOn(KubernetesAiAgentService, "create")
|
|
217
|
+
.mockImplementation(async (createBy: { data: KubernetesAiAgent }) => {
|
|
218
|
+
const created: KubernetesAiAgent = createBy.data;
|
|
219
|
+
created.id = AGENT_ID;
|
|
220
|
+
return created;
|
|
221
|
+
}) as unknown as SpyCalls;
|
|
222
|
+
|
|
223
|
+
const agentUpdateOneById: SpyCalls = jest
|
|
224
|
+
.spyOn(KubernetesAiAgentService, "updateOneById")
|
|
225
|
+
.mockResolvedValue(1) as unknown as SpyCalls;
|
|
226
|
+
|
|
227
|
+
const agentUpdateColumns: SpyCalls = jest
|
|
228
|
+
.spyOn(KubernetesAiAgentService, "updateColumnsByIdWithoutHooks")
|
|
229
|
+
.mockResolvedValue(undefined) as unknown as SpyCalls;
|
|
230
|
+
|
|
231
|
+
const runnerDeletes: Array<SpyCalls> = (
|
|
232
|
+
["deleteBy", "deleteOneBy", "deleteOneById", "hardDeleteBy"] as const
|
|
233
|
+
).map(
|
|
234
|
+
(method: "deleteBy" | "deleteOneBy" | "deleteOneById" | "hardDeleteBy") => {
|
|
235
|
+
return jest
|
|
236
|
+
.spyOn(RunnerService, method)
|
|
237
|
+
.mockResolvedValue(0) as unknown as SpyCalls;
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
findOrCreateCluster,
|
|
243
|
+
clusterFindOneBy,
|
|
244
|
+
clusterUpdateOneBy,
|
|
245
|
+
clusterUpdateOneById,
|
|
246
|
+
feed,
|
|
247
|
+
legacyLookup,
|
|
248
|
+
legacyOnline,
|
|
249
|
+
agentFindOneBy,
|
|
250
|
+
agentCountBy,
|
|
251
|
+
agentCreate,
|
|
252
|
+
agentUpdateOneById,
|
|
253
|
+
agentUpdateColumns,
|
|
254
|
+
runnerDeletes,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function register(
|
|
259
|
+
overrides: Partial<{
|
|
260
|
+
clusterName: unknown;
|
|
261
|
+
agentVersion: string;
|
|
262
|
+
previousAgentKey: string;
|
|
263
|
+
posture: unknown;
|
|
264
|
+
ingestionKeyId: ObjectID;
|
|
265
|
+
}> = {},
|
|
266
|
+
): Promise<KubernetesAiAgentRegistrationResult> {
|
|
267
|
+
return KubernetesAiAgentService.register({
|
|
268
|
+
projectId: PROJECT_ID,
|
|
269
|
+
clusterName: "prod-us",
|
|
270
|
+
agentVersion: "14.1.0",
|
|
271
|
+
posture: {
|
|
272
|
+
clusterIdentifier: "prod-us",
|
|
273
|
+
inCluster: true,
|
|
274
|
+
allowWrites: false,
|
|
275
|
+
allowNodeOperations: false,
|
|
276
|
+
writeNamespaces: [],
|
|
277
|
+
podNamespace: "oneuptime-agent",
|
|
278
|
+
kubectlVersion: "v1.33.1",
|
|
279
|
+
agentChartVersion: "14.1.0",
|
|
280
|
+
},
|
|
281
|
+
ingestionKeyId: INGESTION_KEY_ID,
|
|
282
|
+
...overrides,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function refusalOf(
|
|
287
|
+
promise: Promise<unknown>,
|
|
288
|
+
): Promise<KubernetesAiAgentRegistrationRefusedException> {
|
|
289
|
+
try {
|
|
290
|
+
await promise;
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (error instanceof KubernetesAiAgentRegistrationRefusedException) {
|
|
293
|
+
return error;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
throw new Error("Expected the registration to be refused");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function createdRow(harness: Harness): KubernetesAiAgent {
|
|
303
|
+
expect(harness.agentCreate.mock.calls).toHaveLength(1);
|
|
304
|
+
return (harness.agentCreate.mock.calls[0]![0] as { data: KubernetesAiAgent })
|
|
305
|
+
.data;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function reKeyWrite(harness: Harness): {
|
|
309
|
+
id: ObjectID;
|
|
310
|
+
data: Record<string, unknown>;
|
|
311
|
+
props: Record<string, unknown>;
|
|
312
|
+
} {
|
|
313
|
+
expect(harness.agentUpdateOneById.mock.calls).toHaveLength(1);
|
|
314
|
+
return harness.agentUpdateOneById.mock.calls[0]![0] as {
|
|
315
|
+
id: ObjectID;
|
|
316
|
+
data: Record<string, unknown>;
|
|
317
|
+
props: Record<string, unknown>;
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function nothingWrittenToTheAgentTable(harness: Harness): void {
|
|
322
|
+
expect(harness.agentCreate.mock.calls).toHaveLength(0);
|
|
323
|
+
expect(harness.agentUpdateOneById.mock.calls).toHaveLength(0);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
afterEach(() => {
|
|
327
|
+
jest.restoreAllMocks();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe("register: a cluster's first agent", () => {
|
|
331
|
+
test("creates the row and returns the key, storing only the key's sha256", async () => {
|
|
332
|
+
const harness: Harness = setUp();
|
|
333
|
+
|
|
334
|
+
const result: KubernetesAiAgentRegistrationResult = await register();
|
|
335
|
+
|
|
336
|
+
expect(result.admission).toBe("created");
|
|
337
|
+
expect(result.agentId.toString()).toBe(AGENT_ID.toString());
|
|
338
|
+
expect(result.clusterId.toString()).toBe(CLUSTER_ID.toString());
|
|
339
|
+
expect(result.agentKey).toMatch(/^[0-9a-f]{64}$/);
|
|
340
|
+
|
|
341
|
+
const row: KubernetesAiAgent = createdRow(harness);
|
|
342
|
+
expect(row.projectId!.toString()).toBe(PROJECT_ID.toString());
|
|
343
|
+
expect(row.kubernetesClusterId!.toString()).toBe(CLUSTER_ID.toString());
|
|
344
|
+
expect(row.keyHash).toBe(
|
|
345
|
+
KubernetesAiAgentServiceClass.hashKey(result.agentKey),
|
|
346
|
+
);
|
|
347
|
+
expect(row.keyHash).not.toBe(result.agentKey);
|
|
348
|
+
expect(Object.values(row)).not.toContain(result.agentKey);
|
|
349
|
+
|
|
350
|
+
const createBy: { props: Record<string, unknown> } = harness.agentCreate
|
|
351
|
+
.mock.calls[0]![0] as { props: Record<string, unknown> };
|
|
352
|
+
expect(createBy.props["isRoot"]).toBe(true);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("the new row is connected, alive and registered now, with the agent's version and the ingestion key that minted it", async () => {
|
|
356
|
+
const harness: Harness = setUp();
|
|
357
|
+
const before: number = Date.now();
|
|
358
|
+
|
|
359
|
+
await register();
|
|
360
|
+
|
|
361
|
+
const row: KubernetesAiAgent = createdRow(harness);
|
|
362
|
+
expect(row.connectionStatus).toBe("connected");
|
|
363
|
+
expect(row.lastAliveAt!.getTime()).toBeGreaterThanOrEqual(before);
|
|
364
|
+
expect(row.lastRegisteredAt!.getTime()).toBeGreaterThanOrEqual(before);
|
|
365
|
+
expect(row.agentVersion).toBe("14.1.0");
|
|
366
|
+
expect(row.registeredWithIngestionKeyId!.toString()).toBe(
|
|
367
|
+
INGESTION_KEY_ID.toString(),
|
|
368
|
+
);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("a registration without an ingestion key id or version leaves them unset", async () => {
|
|
372
|
+
const harness: Harness = setUp();
|
|
373
|
+
|
|
374
|
+
await KubernetesAiAgentService.register({
|
|
375
|
+
projectId: PROJECT_ID,
|
|
376
|
+
clusterName: "prod-us",
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const row: KubernetesAiAgent = createdRow(harness);
|
|
380
|
+
expect(row.registeredWithIngestionKeyId).toBeUndefined();
|
|
381
|
+
expect(row.agentVersion).toBeUndefined();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("every registration mints a different key", async () => {
|
|
385
|
+
setUp();
|
|
386
|
+
|
|
387
|
+
const first: KubernetesAiAgentRegistrationResult = await register();
|
|
388
|
+
const second: KubernetesAiAgentRegistrationResult = await register();
|
|
389
|
+
|
|
390
|
+
expect(first.agentKey).not.toBe(second.agentKey);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("finds or creates the cluster by the trimmed name, in the registering project, and re-reads its AI columns as root", async () => {
|
|
394
|
+
const harness: Harness = setUp();
|
|
395
|
+
|
|
396
|
+
await register({ clusterName: " prod-us " });
|
|
397
|
+
|
|
398
|
+
expect(harness.findOrCreateCluster.mock.calls[0]![0]).toEqual({
|
|
399
|
+
projectId: PROJECT_ID,
|
|
400
|
+
clusterIdentifier: "prod-us",
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const read: {
|
|
404
|
+
query: Record<string, unknown>;
|
|
405
|
+
select: Record<string, boolean>;
|
|
406
|
+
props: Record<string, unknown>;
|
|
407
|
+
} = harness.clusterFindOneBy.mock.calls[0]![0] as {
|
|
408
|
+
query: Record<string, unknown>;
|
|
409
|
+
select: Record<string, boolean>;
|
|
410
|
+
props: Record<string, unknown>;
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
expect(read.query["_id"]).toBe(CLUSTER_ID.toString());
|
|
414
|
+
expect(read.query["projectId"]).toBe(PROJECT_ID);
|
|
415
|
+
expect(read.select["aiAccessConfiguredAt"]).toBe(true);
|
|
416
|
+
expect(read.select["isAiInvestigationEnabled"]).toBe(true);
|
|
417
|
+
expect(read.select["aiRemediationMode"]).toBe(true);
|
|
418
|
+
expect(read.select["clusterIdentifier"]).toBe(true);
|
|
419
|
+
expect(read.props["isRoot"]).toBe(true);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("looks the agent row up by project and cluster, as root, selecting the key hash", async () => {
|
|
423
|
+
const harness: Harness = setUp();
|
|
424
|
+
|
|
425
|
+
await register();
|
|
426
|
+
|
|
427
|
+
const lookup: {
|
|
428
|
+
query: Record<string, unknown>;
|
|
429
|
+
select: Record<string, boolean>;
|
|
430
|
+
props: Record<string, unknown>;
|
|
431
|
+
} = harness.agentFindOneBy.mock.calls[0]![0] as {
|
|
432
|
+
query: Record<string, unknown>;
|
|
433
|
+
select: Record<string, boolean>;
|
|
434
|
+
props: Record<string, unknown>;
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
expect(lookup.query["projectId"]).toBe(PROJECT_ID);
|
|
438
|
+
expect(String(lookup.query["kubernetesClusterId"])).toBe(
|
|
439
|
+
CLUSTER_ID.toString(),
|
|
440
|
+
);
|
|
441
|
+
expect(lookup.select).toEqual({
|
|
442
|
+
...KUBERNETES_AI_AGENT_SELECT_WITHOUT_KEY,
|
|
443
|
+
keyHash: true,
|
|
444
|
+
});
|
|
445
|
+
expect(lookup.props["isRoot"]).toBe(true);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test("the version is trimmed and bounded to its column", async () => {
|
|
449
|
+
const harness: Harness = setUp();
|
|
450
|
+
|
|
451
|
+
await register({ agentVersion: ` ${"9".repeat(300)} ` });
|
|
452
|
+
|
|
453
|
+
expect(createdRow(harness).agentVersion).toBe(
|
|
454
|
+
"9".repeat(ColumnLength.ShortText),
|
|
455
|
+
);
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
describe("register: the stored posture", () => {
|
|
460
|
+
test("keeps what the agent reported and forces in-cluster and the cluster", async () => {
|
|
461
|
+
const harness: Harness = setUp();
|
|
462
|
+
|
|
463
|
+
await register({
|
|
464
|
+
posture: {
|
|
465
|
+
clusterIdentifier: "prod-us",
|
|
466
|
+
inCluster: true,
|
|
467
|
+
allowWrites: true,
|
|
468
|
+
allowNodeOperations: false,
|
|
469
|
+
writeNamespaces: ["web", "api"],
|
|
470
|
+
podNamespace: "oneuptime-agent",
|
|
471
|
+
kubectlVersion: "v1.33.1",
|
|
472
|
+
agentChartVersion: "14.1.0",
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
expect(createdRow(harness).posture).toEqual({
|
|
477
|
+
clusterIdentifier: "prod-us",
|
|
478
|
+
inCluster: true,
|
|
479
|
+
allowWrites: true,
|
|
480
|
+
allowNodeOperations: false,
|
|
481
|
+
writeNamespaces: ["web", "api"],
|
|
482
|
+
podNamespace: "oneuptime-agent",
|
|
483
|
+
kubectlVersion: "v1.33.1",
|
|
484
|
+
agentChartVersion: "14.1.0",
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
/*
|
|
489
|
+
* A body can never make the agent the in-cluster executor of another
|
|
490
|
+
* cluster: the cluster is the one it registered for, whatever the posture
|
|
491
|
+
* claims.
|
|
492
|
+
*/
|
|
493
|
+
test("a posture that names another cluster, or says it is not in a cluster, is overridden", async () => {
|
|
494
|
+
const harness: Harness = setUp();
|
|
495
|
+
|
|
496
|
+
await register({
|
|
497
|
+
posture: { clusterIdentifier: "someone-else", inCluster: false },
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
const posture: JSONObject = createdRow(harness).posture!;
|
|
501
|
+
expect(posture["clusterIdentifier"]).toBe("prod-us");
|
|
502
|
+
expect(posture["inCluster"]).toBe(true);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("the cluster is named the way the cluster row spells it", async () => {
|
|
506
|
+
const harness: Harness = setUp({
|
|
507
|
+
cluster: makeCluster({ clusterIdentifier: "Prod-US" }),
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
await register({ clusterName: "prod-us" });
|
|
511
|
+
|
|
512
|
+
expect(createdRow(harness).posture!["clusterIdentifier"]).toBe("Prod-US");
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test("malformed fields are dropped, never trusted — writes stay off", async () => {
|
|
516
|
+
const harness: Harness = setUp();
|
|
517
|
+
|
|
518
|
+
await register({
|
|
519
|
+
posture: {
|
|
520
|
+
allowWrites: "true",
|
|
521
|
+
writeNamespaces: ["web", "", 42, null],
|
|
522
|
+
podNamespace: 7,
|
|
523
|
+
allowNodeOperations: "false",
|
|
524
|
+
kubectlVersion: { v: 1 },
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
const posture: JSONObject = createdRow(harness).posture!;
|
|
529
|
+
expect(posture["allowWrites"]).toBe(false);
|
|
530
|
+
expect(posture["writeNamespaces"]).toEqual(["web"]);
|
|
531
|
+
expect(posture).not.toHaveProperty("podNamespace");
|
|
532
|
+
expect(posture).not.toHaveProperty("allowNodeOperations");
|
|
533
|
+
expect(posture).not.toHaveProperty("kubectlVersion");
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test("no posture at all is a read-only in-cluster agent of this cluster", async () => {
|
|
537
|
+
const harness: Harness = setUp();
|
|
538
|
+
|
|
539
|
+
await register({ posture: undefined });
|
|
540
|
+
|
|
541
|
+
expect(createdRow(harness).posture).toEqual({
|
|
542
|
+
clusterIdentifier: "prod-us",
|
|
543
|
+
inCluster: true,
|
|
544
|
+
allowWrites: false,
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
describe("register: cluster_name_invalid", () => {
|
|
550
|
+
test.each([
|
|
551
|
+
["an empty name", ""],
|
|
552
|
+
["a blank name", " "],
|
|
553
|
+
["no name", undefined],
|
|
554
|
+
["a number", 42],
|
|
555
|
+
["an object", { name: "prod-us" }],
|
|
556
|
+
])(
|
|
557
|
+
"%s is refused before anything is read or written",
|
|
558
|
+
async (_label: string, clusterName: unknown) => {
|
|
559
|
+
const harness: Harness = setUp();
|
|
560
|
+
|
|
561
|
+
const refusal: KubernetesAiAgentRegistrationRefusedException =
|
|
562
|
+
await refusalOf(register({ clusterName }));
|
|
563
|
+
|
|
564
|
+
expect(refusal.reason).toBe("cluster_name_invalid");
|
|
565
|
+
expect(refusal.retryAfterSeconds).toBeUndefined();
|
|
566
|
+
expect(refusal.message).toContain("clusterName");
|
|
567
|
+
expect(harness.findOrCreateCluster.mock.calls).toHaveLength(0);
|
|
568
|
+
nothingWrittenToTheAgentTable(harness);
|
|
569
|
+
},
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
test("a name longer than the cluster identifier column is refused, saying the limit", async () => {
|
|
573
|
+
const harness: Harness = setUp();
|
|
574
|
+
const tooLong: string = "c".repeat(
|
|
575
|
+
MAX_KUBERNETES_AI_AGENT_CLUSTER_NAME_LENGTH + 1,
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
const refusal: KubernetesAiAgentRegistrationRefusedException =
|
|
579
|
+
await refusalOf(register({ clusterName: tooLong }));
|
|
580
|
+
|
|
581
|
+
expect(refusal.reason).toBe("cluster_name_invalid");
|
|
582
|
+
expect(refusal.message).toContain(
|
|
583
|
+
String(MAX_KUBERNETES_AI_AGENT_CLUSTER_NAME_LENGTH),
|
|
584
|
+
);
|
|
585
|
+
expect(harness.findOrCreateCluster.mock.calls).toHaveLength(0);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test("a name exactly at the limit is accepted", async () => {
|
|
589
|
+
const harness: Harness = setUp();
|
|
590
|
+
|
|
591
|
+
await register({
|
|
592
|
+
clusterName: "c".repeat(MAX_KUBERNETES_AI_AGENT_CLUSTER_NAME_LENGTH),
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
expect(harness.agentCreate.mock.calls).toHaveLength(1);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test("the limit is the cluster identifier column's", () => {
|
|
599
|
+
expect(MAX_KUBERNETES_AI_AGENT_CLUSTER_NAME_LENGTH).toBe(
|
|
600
|
+
ColumnLength.ShortText,
|
|
601
|
+
);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test("is a 403 refusal that needs an operator", async () => {
|
|
605
|
+
setUp();
|
|
606
|
+
|
|
607
|
+
const refusal: KubernetesAiAgentRegistrationRefusedException =
|
|
608
|
+
await refusalOf(register({ clusterName: "" }));
|
|
609
|
+
|
|
610
|
+
expect(refusal).toBeInstanceOf(ForbiddenException);
|
|
611
|
+
expect(
|
|
612
|
+
isTransientKubernetesAiAgentRegistrationRefusal(refusal.reason),
|
|
613
|
+
).toBe(false);
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
describe("register: the previous in-cluster Runner is online (legacy_runner_online)", () => {
|
|
618
|
+
test("the agent is asked to wait 20 seconds, and nothing is written", async () => {
|
|
619
|
+
const harness: Harness = setUp({
|
|
620
|
+
legacyRunner: makeLegacyRunner(),
|
|
621
|
+
legacyOnline: true,
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
const refusal: KubernetesAiAgentRegistrationRefusedException =
|
|
625
|
+
await refusalOf(register());
|
|
626
|
+
|
|
627
|
+
expect(refusal.reason).toBe("legacy_runner_online");
|
|
628
|
+
expect(refusal.retryAfterSeconds).toBe(20);
|
|
629
|
+
expect(KUBERNETES_AI_AGENT_REGISTRATION_RETRY_AFTER_SECONDS).toBe(20);
|
|
630
|
+
expect(
|
|
631
|
+
isTransientKubernetesAiAgentRegistrationRefusal(refusal.reason),
|
|
632
|
+
).toBe(true);
|
|
633
|
+
expect(refusal.message).toContain('"prod-us"');
|
|
634
|
+
expect(refusal.message).toContain("helm upgrade");
|
|
635
|
+
|
|
636
|
+
nothingWrittenToTheAgentTable(harness);
|
|
637
|
+
expect(harness.clusterUpdateOneBy.mock.calls).toHaveLength(0);
|
|
638
|
+
expect(harness.feed.mock.calls).toHaveLength(0);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
test("looks the Runner up for this project, cluster and identifier, and asks the one presence rule", async () => {
|
|
642
|
+
const legacy: Runner = makeLegacyRunner();
|
|
643
|
+
const harness: Harness = setUp({
|
|
644
|
+
legacyRunner: legacy,
|
|
645
|
+
legacyOnline: true,
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
await refusalOf(register());
|
|
649
|
+
|
|
650
|
+
expect(harness.legacyLookup.mock.calls[0]![0]).toEqual({
|
|
651
|
+
projectId: PROJECT_ID,
|
|
652
|
+
kubernetesClusterId: CLUSTER_ID,
|
|
653
|
+
clusterIdentifier: "prod-us",
|
|
654
|
+
});
|
|
655
|
+
expect(harness.legacyOnline.mock.calls[0]![0]).toBe(legacy);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// An ingestion key alone can never evict a live Runner, even with a key.
|
|
659
|
+
test("refuses even an agent that proves continuity with its current key", async () => {
|
|
660
|
+
const harness: Harness = setUp({
|
|
661
|
+
existing: makeAgent(),
|
|
662
|
+
legacyRunner: makeLegacyRunner(),
|
|
663
|
+
legacyOnline: true,
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
const refusal: KubernetesAiAgentRegistrationRefusedException =
|
|
667
|
+
await refusalOf(register({ previousAgentKey: CURRENT_KEY }));
|
|
668
|
+
|
|
669
|
+
expect(refusal.reason).toBe("legacy_runner_online");
|
|
670
|
+
nothingWrittenToTheAgentTable(harness);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
test("it is not recorded as another agent's attempt on the row", async () => {
|
|
674
|
+
const harness: Harness = setUp({
|
|
675
|
+
existing: makeAgent(),
|
|
676
|
+
legacyRunner: makeLegacyRunner(),
|
|
677
|
+
legacyOnline: true,
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
await refusalOf(register());
|
|
681
|
+
|
|
682
|
+
expect(harness.agentUpdateColumns.mock.calls).toHaveLength(0);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
test("an offline previous Runner does not hold the cluster", async () => {
|
|
686
|
+
const harness: Harness = setUp({
|
|
687
|
+
legacyRunner: makeLegacyRunner(),
|
|
688
|
+
legacyOnline: false,
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
const result: KubernetesAiAgentRegistrationResult = await register();
|
|
692
|
+
|
|
693
|
+
expect(result.admission).toBe("created");
|
|
694
|
+
expect(harness.agentCreate.mock.calls).toHaveLength(1);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
test("a cluster that never had a Runner registers straight away", async () => {
|
|
698
|
+
const harness: Harness = setUp({ legacyRunner: null });
|
|
699
|
+
|
|
700
|
+
await register();
|
|
701
|
+
|
|
702
|
+
expect(harness.legacyOnline.mock.calls).toHaveLength(0);
|
|
703
|
+
expect(harness.agentCreate.mock.calls).toHaveLength(1);
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
// The agent replaces the Runner without ever touching it.
|
|
707
|
+
test("never deletes, re-keys or unbinds the previous Runner", async () => {
|
|
708
|
+
const harness: Harness = setUp({
|
|
709
|
+
legacyRunner: makeLegacyRunner(),
|
|
710
|
+
legacyOnline: false,
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
await register();
|
|
714
|
+
|
|
715
|
+
for (const spy of harness.runnerDeletes) {
|
|
716
|
+
expect(spy.mock.calls).toHaveLength(0);
|
|
717
|
+
}
|
|
718
|
+
expect(harness.clusterUpdateOneById.mock.calls).toHaveLength(0);
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
describe("register: re-registering an existing agent", () => {
|
|
723
|
+
test("continuity: the current key re-keys even a live agent", async () => {
|
|
724
|
+
const harness: Harness = setUp({ existing: makeAgent() });
|
|
725
|
+
|
|
726
|
+
const result: KubernetesAiAgentRegistrationResult = await register({
|
|
727
|
+
previousAgentKey: CURRENT_KEY,
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
expect(result.admission).toBe("continuity");
|
|
731
|
+
expect(result.agentId.toString()).toBe(AGENT_ID.toString());
|
|
732
|
+
expect(result.agentKey).not.toBe(CURRENT_KEY);
|
|
733
|
+
|
|
734
|
+
const write: { id: ObjectID; data: Record<string, unknown> } =
|
|
735
|
+
reKeyWrite(harness);
|
|
736
|
+
expect(write.id.toString()).toBe(AGENT_ID.toString());
|
|
737
|
+
expect(write.data["keyHash"]).toBe(
|
|
738
|
+
KubernetesAiAgentServiceClass.hashKey(result.agentKey),
|
|
739
|
+
);
|
|
740
|
+
expect(harness.agentCreate.mock.calls).toHaveLength(0);
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
test("the re-key writes the new hash, connected, alive and registered now, the posture and the minting key", async () => {
|
|
744
|
+
const harness: Harness = setUp({
|
|
745
|
+
existing: makeAgent({
|
|
746
|
+
connectionStatus: "disconnected",
|
|
747
|
+
lastAliveAt: msAgo(ALIVE_WINDOW_MS * 3),
|
|
748
|
+
}),
|
|
749
|
+
});
|
|
750
|
+
const before: number = Date.now();
|
|
751
|
+
|
|
752
|
+
await register({ agentVersion: "14.2.0" });
|
|
753
|
+
|
|
754
|
+
const write: {
|
|
755
|
+
data: Record<string, unknown>;
|
|
756
|
+
props: Record<string, unknown>;
|
|
757
|
+
} = reKeyWrite(harness);
|
|
758
|
+
|
|
759
|
+
expect(write.props["isRoot"]).toBe(true);
|
|
760
|
+
expect(write.data["connectionStatus"]).toBe("connected");
|
|
761
|
+
expect(
|
|
762
|
+
(write.data["lastAliveAt"] as Date).getTime(),
|
|
763
|
+
).toBeGreaterThanOrEqual(before);
|
|
764
|
+
expect(
|
|
765
|
+
(write.data["lastRegisteredAt"] as Date).getTime(),
|
|
766
|
+
).toBeGreaterThanOrEqual(before);
|
|
767
|
+
expect(write.data["agentVersion"]).toBe("14.2.0");
|
|
768
|
+
expect(String(write.data["registeredWithIngestionKeyId"])).toBe(
|
|
769
|
+
INGESTION_KEY_ID.toString(),
|
|
770
|
+
);
|
|
771
|
+
expect((write.data["posture"] as JSONObject)["clusterIdentifier"]).toBe(
|
|
772
|
+
"prod-us",
|
|
773
|
+
);
|
|
774
|
+
expect((write.data["posture"] as JSONObject)["inCluster"]).toBe(true);
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
test("a re-key without an ingestion key id clears the previous one, and keeps the stored version when none is sent", async () => {
|
|
778
|
+
const harness: Harness = setUp({
|
|
779
|
+
existing: makeAgent({ connectionStatus: "disconnected" }),
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
await KubernetesAiAgentService.register({
|
|
783
|
+
projectId: PROJECT_ID,
|
|
784
|
+
clusterName: "prod-us",
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
const write: { data: Record<string, unknown> } = reKeyWrite(harness);
|
|
788
|
+
expect(write.data["registeredWithIngestionKeyId"]).toBeNull();
|
|
789
|
+
expect(write.data).not.toHaveProperty("agentVersion");
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
test("offline: an agent that stopped heartbeating is replaced without its key", async () => {
|
|
793
|
+
const harness: Harness = setUp({
|
|
794
|
+
existing: makeAgent({ lastAliveAt: msAgo(ALIVE_WINDOW_MS + 60 * 1000) }),
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
const result: KubernetesAiAgentRegistrationResult = await register();
|
|
798
|
+
|
|
799
|
+
expect(result.admission).toBe("offline");
|
|
800
|
+
expect(harness.agentUpdateOneById.mock.calls).toHaveLength(1);
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
test("signed off: an agent that said goodbye is replaced at once", async () => {
|
|
804
|
+
const harness: Harness = setUp({
|
|
805
|
+
existing: makeAgent({
|
|
806
|
+
connectionStatus: "disconnected",
|
|
807
|
+
lastAliveAt: msAgo(5 * 1000),
|
|
808
|
+
}),
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
const result: KubernetesAiAgentRegistrationResult = await register();
|
|
812
|
+
|
|
813
|
+
expect(result.admission).toBe("signed_off");
|
|
814
|
+
expect(harness.agentUpdateOneById.mock.calls).toHaveLength(1);
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
test("reset: a row an admin reset admits the next registration, even if it still reads as connected", async () => {
|
|
818
|
+
const harness: Harness = setUp({
|
|
819
|
+
existing: makeAgent({
|
|
820
|
+
keyHash: null,
|
|
821
|
+
connectionStatus: "connected",
|
|
822
|
+
lastAliveAt: msAgo(5 * 1000),
|
|
823
|
+
}),
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
const result: KubernetesAiAgentRegistrationResult = await register();
|
|
827
|
+
|
|
828
|
+
expect(result.admission).toBe("reset");
|
|
829
|
+
expect(reKeyWrite(harness).data["keyHash"]).toBe(
|
|
830
|
+
KubernetesAiAgentServiceClass.hashKey(result.agentKey),
|
|
831
|
+
);
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
test("the caps are not consulted for an existing row", async () => {
|
|
835
|
+
const harness: Harness = setUp({
|
|
836
|
+
existing: makeAgent({ connectionStatus: "disconnected" }),
|
|
837
|
+
totalAgents: MAX_KUBERNETES_AI_AGENTS_PER_PROJECT,
|
|
838
|
+
agentsInLastHour: MAX_NEW_KUBERNETES_AI_AGENTS_PER_PROJECT_PER_HOUR,
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
await register();
|
|
842
|
+
|
|
843
|
+
expect(harness.agentCountBy.mock.calls).toHaveLength(0);
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
test("no connected feed item for a re-key that changed no setting", async () => {
|
|
847
|
+
const harness: Harness = setUp({
|
|
848
|
+
existing: makeAgent({ connectionStatus: "disconnected" }),
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
await register();
|
|
852
|
+
|
|
853
|
+
expect(harness.feed.mock.calls).toHaveLength(0);
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
describe("register: another agent is online (previous_instance_online)", () => {
|
|
858
|
+
test.each([
|
|
859
|
+
["no key", undefined],
|
|
860
|
+
["a wrong key", "cd".repeat(32)],
|
|
861
|
+
[
|
|
862
|
+
"the key's hash instead of the key",
|
|
863
|
+
KubernetesAiAgentServiceClass.hashKey(CURRENT_KEY),
|
|
864
|
+
],
|
|
865
|
+
])(
|
|
866
|
+
"%s: refused, transient, 20 seconds — and the key is not changed",
|
|
867
|
+
async (_label: string, previousAgentKey: string | undefined) => {
|
|
868
|
+
const harness: Harness = setUp({ existing: makeAgent() });
|
|
869
|
+
|
|
870
|
+
const refusal: KubernetesAiAgentRegistrationRefusedException =
|
|
871
|
+
await refusalOf(register(previousAgentKey ? { previousAgentKey } : {}));
|
|
872
|
+
|
|
873
|
+
expect(refusal.reason).toBe("previous_instance_online");
|
|
874
|
+
expect(refusal.retryAfterSeconds).toBe(20);
|
|
875
|
+
expect(
|
|
876
|
+
isTransientKubernetesAiAgentRegistrationRefusal(refusal.reason),
|
|
877
|
+
).toBe(true);
|
|
878
|
+
expect(refusal.message).toContain('"prod-us"');
|
|
879
|
+
nothingWrittenToTheAgentTable(harness);
|
|
880
|
+
expect(harness.clusterUpdateOneBy.mock.calls).toHaveLength(0);
|
|
881
|
+
expect(harness.feed.mock.calls).toHaveLength(0);
|
|
882
|
+
},
|
|
883
|
+
);
|
|
884
|
+
|
|
885
|
+
test("the refusal is recorded on the row: when, and why", async () => {
|
|
886
|
+
const harness: Harness = setUp({ existing: makeAgent() });
|
|
887
|
+
const before: number = Date.now();
|
|
888
|
+
|
|
889
|
+
await refusalOf(register());
|
|
890
|
+
|
|
891
|
+
expect(harness.agentUpdateColumns.mock.calls).toHaveLength(1);
|
|
892
|
+
|
|
893
|
+
const record: { id: ObjectID; data: Record<string, unknown> } = harness
|
|
894
|
+
.agentUpdateColumns.mock.calls[0]![0] as {
|
|
895
|
+
id: ObjectID;
|
|
896
|
+
data: Record<string, unknown>;
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
expect(record.id.toString()).toBe(AGENT_ID.toString());
|
|
900
|
+
expect(record.data["lastRefusedRegistrationReason"]).toBe(
|
|
901
|
+
"previous_instance_online",
|
|
902
|
+
);
|
|
903
|
+
expect(
|
|
904
|
+
(record.data["lastRefusedRegistrationAt"] as Date).getTime(),
|
|
905
|
+
).toBeGreaterThanOrEqual(before);
|
|
906
|
+
expect(Object.keys(record.data).sort()).toEqual([
|
|
907
|
+
"lastRefusedRegistrationAt",
|
|
908
|
+
"lastRefusedRegistrationReason",
|
|
909
|
+
]);
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
test("a failure to record it never turns the refusal into a server error", async () => {
|
|
913
|
+
const harness: Harness = setUp({ existing: makeAgent() });
|
|
914
|
+
(
|
|
915
|
+
harness.agentUpdateColumns as unknown as {
|
|
916
|
+
mockRejectedValue: (error: Error) => void;
|
|
917
|
+
}
|
|
918
|
+
).mockRejectedValue(new Error("database is down"));
|
|
919
|
+
|
|
920
|
+
const refusal: KubernetesAiAgentRegistrationRefusedException =
|
|
921
|
+
await refusalOf(register());
|
|
922
|
+
|
|
923
|
+
expect(refusal.reason).toBe("previous_instance_online");
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
test("an agent heard from just inside the alive window is still online", async () => {
|
|
927
|
+
setUp({
|
|
928
|
+
existing: makeAgent({ lastAliveAt: msAgo(ALIVE_WINDOW_MS - 10 * 1000) }),
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
const refusal: KubernetesAiAgentRegistrationRefusedException =
|
|
932
|
+
await refusalOf(register());
|
|
933
|
+
|
|
934
|
+
expect(refusal.reason).toBe("previous_instance_online");
|
|
935
|
+
});
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
describe("register: agent_cap_reached", () => {
|
|
939
|
+
test(`a project with ${MAX_KUBERNETES_AI_AGENTS_PER_PROJECT} agents gets no new one`, async () => {
|
|
940
|
+
const harness: Harness = setUp({
|
|
941
|
+
totalAgents: MAX_KUBERNETES_AI_AGENTS_PER_PROJECT,
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
const refusal: KubernetesAiAgentRegistrationRefusedException =
|
|
945
|
+
await refusalOf(register());
|
|
946
|
+
|
|
947
|
+
expect(refusal.reason).toBe("agent_cap_reached");
|
|
948
|
+
expect(refusal.retryAfterSeconds).toBeUndefined();
|
|
949
|
+
expect(
|
|
950
|
+
isTransientKubernetesAiAgentRegistrationRefusal(refusal.reason),
|
|
951
|
+
).toBe(false);
|
|
952
|
+
expect(refusal.message).toContain(
|
|
953
|
+
String(MAX_KUBERNETES_AI_AGENTS_PER_PROJECT),
|
|
954
|
+
);
|
|
955
|
+
nothingWrittenToTheAgentTable(harness);
|
|
956
|
+
expect(harness.feed.mock.calls).toHaveLength(0);
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
test(`${MAX_NEW_KUBERNETES_AI_AGENTS_PER_PROJECT_PER_HOUR} new agents in the last hour is the hourly brake`, async () => {
|
|
960
|
+
const harness: Harness = setUp({
|
|
961
|
+
totalAgents: 40,
|
|
962
|
+
agentsInLastHour: MAX_NEW_KUBERNETES_AI_AGENTS_PER_PROJECT_PER_HOUR,
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
const refusal: KubernetesAiAgentRegistrationRefusedException =
|
|
966
|
+
await refusalOf(register());
|
|
967
|
+
|
|
968
|
+
expect(refusal.reason).toBe("agent_cap_reached");
|
|
969
|
+
expect(refusal.message).toContain(
|
|
970
|
+
String(MAX_NEW_KUBERNETES_AI_AGENTS_PER_PROJECT_PER_HOUR),
|
|
971
|
+
);
|
|
972
|
+
nothingWrittenToTheAgentTable(harness);
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
test("one under either limit is admitted", async () => {
|
|
976
|
+
const harness: Harness = setUp({
|
|
977
|
+
totalAgents: MAX_KUBERNETES_AI_AGENTS_PER_PROJECT - 1,
|
|
978
|
+
agentsInLastHour: MAX_NEW_KUBERNETES_AI_AGENTS_PER_PROJECT_PER_HOUR - 1,
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
await register();
|
|
982
|
+
|
|
983
|
+
expect(harness.agentCreate.mock.calls).toHaveLength(1);
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
test("both counts are scoped to the project, as root; the brake counts only the last hour's rows", async () => {
|
|
987
|
+
const harness: Harness = setUp();
|
|
988
|
+
|
|
989
|
+
await register();
|
|
990
|
+
|
|
991
|
+
expect(harness.agentCountBy.mock.calls).toHaveLength(2);
|
|
992
|
+
|
|
993
|
+
const [total, lastHour] = harness.agentCountBy.mock.calls.map(
|
|
994
|
+
(call: Array<unknown>) => {
|
|
995
|
+
return call[0] as {
|
|
996
|
+
query: Record<string, unknown>;
|
|
997
|
+
props: Record<string, unknown>;
|
|
998
|
+
};
|
|
999
|
+
},
|
|
1000
|
+
) as [
|
|
1001
|
+
{ query: Record<string, unknown>; props: Record<string, unknown> },
|
|
1002
|
+
{ query: Record<string, unknown>; props: Record<string, unknown> },
|
|
1003
|
+
];
|
|
1004
|
+
|
|
1005
|
+
expect(total.query).toEqual({ projectId: PROJECT_ID });
|
|
1006
|
+
expect(total.props["isRoot"]).toBe(true);
|
|
1007
|
+
expect(lastHour.query["projectId"]).toBe(PROJECT_ID);
|
|
1008
|
+
expect(lastHour.query["createdAt"]).toBeDefined();
|
|
1009
|
+
expect(lastHour.props["isRoot"]).toBe(true);
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
test("the caps match the in-cluster Runner's", () => {
|
|
1013
|
+
expect(MAX_KUBERNETES_AI_AGENTS_PER_PROJECT).toBe(250);
|
|
1014
|
+
expect(MAX_NEW_KUBERNETES_AI_AGENTS_PER_PROJECT_PER_HOUR).toBe(30);
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
describe("register: two pods registering a new cluster at once", () => {
|
|
1019
|
+
test("the one whose create loses is told the other agent is online", async () => {
|
|
1020
|
+
const harness: Harness = setUp();
|
|
1021
|
+
(
|
|
1022
|
+
harness.agentCreate as unknown as {
|
|
1023
|
+
mockRejectedValue: (error: Error) => void;
|
|
1024
|
+
}
|
|
1025
|
+
).mockRejectedValue(new Error("duplicate key value"));
|
|
1026
|
+
(
|
|
1027
|
+
harness.agentFindOneBy as unknown as {
|
|
1028
|
+
mockResolvedValueOnce: (value: unknown) => {
|
|
1029
|
+
mockResolvedValueOnce: (value: unknown) => void;
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
)
|
|
1033
|
+
.mockResolvedValueOnce(null)
|
|
1034
|
+
.mockResolvedValueOnce(makeAgent());
|
|
1035
|
+
|
|
1036
|
+
const refusal: KubernetesAiAgentRegistrationRefusedException =
|
|
1037
|
+
await refusalOf(register());
|
|
1038
|
+
|
|
1039
|
+
expect(refusal.reason).toBe("previous_instance_online");
|
|
1040
|
+
expect(refusal.retryAfterSeconds).toBe(20);
|
|
1041
|
+
expect(harness.feed.mock.calls).toHaveLength(0);
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
test("any other create failure is not dressed up as a refusal", async () => {
|
|
1045
|
+
const harness: Harness = setUp();
|
|
1046
|
+
(
|
|
1047
|
+
harness.agentCreate as unknown as {
|
|
1048
|
+
mockRejectedValue: (error: Error) => void;
|
|
1049
|
+
}
|
|
1050
|
+
).mockRejectedValue(new Error("connection reset"));
|
|
1051
|
+
|
|
1052
|
+
await expect(register()).rejects.toThrow("connection reset");
|
|
1053
|
+
});
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
describe("register: first-connection defaults", () => {
|
|
1057
|
+
function defaultsWrite(harness: Harness): {
|
|
1058
|
+
query: Record<string, unknown>;
|
|
1059
|
+
data: Record<string, unknown>;
|
|
1060
|
+
props: Record<string, unknown>;
|
|
1061
|
+
} {
|
|
1062
|
+
expect(harness.clusterUpdateOneBy.mock.calls).toHaveLength(1);
|
|
1063
|
+
return harness.clusterUpdateOneBy.mock.calls[0]![0] as {
|
|
1064
|
+
query: Record<string, unknown>;
|
|
1065
|
+
data: Record<string, unknown>;
|
|
1066
|
+
props: Record<string, unknown>;
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
test("a cluster nobody configured gets investigation on", async () => {
|
|
1071
|
+
const harness: Harness = setUp({
|
|
1072
|
+
cluster: makeCluster({ isAiInvestigationEnabled: false }),
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
await register();
|
|
1076
|
+
|
|
1077
|
+
const write: {
|
|
1078
|
+
query: Record<string, unknown>;
|
|
1079
|
+
data: Record<string, unknown>;
|
|
1080
|
+
props: Record<string, unknown>;
|
|
1081
|
+
} = defaultsWrite(harness);
|
|
1082
|
+
|
|
1083
|
+
expect(write.data).toEqual({ isAiInvestigationEnabled: true });
|
|
1084
|
+
expect(write.query["_id"]).toBe(CLUSTER_ID.toString());
|
|
1085
|
+
expect(write.query["projectId"]).toBe(PROJECT_ID);
|
|
1086
|
+
expect(write.props["isRoot"]).toBe(true);
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
// Chart write RBAC is the cluster operator's consent; a human still approves.
|
|
1090
|
+
test("and, when the chart granted writes, fixes Ask for approval instead of Off", async () => {
|
|
1091
|
+
const harness: Harness = setUp({
|
|
1092
|
+
cluster: makeCluster({ isAiInvestigationEnabled: false }),
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
await register({
|
|
1096
|
+
posture: { allowWrites: true, writeNamespaces: ["web"] },
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
expect(defaultsWrite(harness).data).toEqual({
|
|
1100
|
+
isAiInvestigationEnabled: true,
|
|
1101
|
+
aiRemediationMode: KubernetesAiRemediationMode.RequireApproval,
|
|
1102
|
+
});
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
test("the update only lands while the cluster is still unconfigured", async () => {
|
|
1106
|
+
const harness: Harness = setUp({
|
|
1107
|
+
cluster: makeCluster({ isAiInvestigationEnabled: false }),
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
await register();
|
|
1111
|
+
|
|
1112
|
+
const query: Record<string, unknown> = defaultsWrite(harness).query;
|
|
1113
|
+
const configuredAt: FindOperator<unknown> = query[
|
|
1114
|
+
"aiAccessConfiguredAt"
|
|
1115
|
+
] as FindOperator<unknown>;
|
|
1116
|
+
|
|
1117
|
+
// QueryHelper.isNull: a raw `IS NULL` on the column.
|
|
1118
|
+
expect(configuredAt).toBeInstanceOf(FindOperator);
|
|
1119
|
+
expect(configuredAt.type).toBe("raw");
|
|
1120
|
+
expect((configuredAt.getSql as (alias: string) => string)("column")).toBe(
|
|
1121
|
+
"(column IS NULL)",
|
|
1122
|
+
);
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
test("never marks the cluster configured", async () => {
|
|
1126
|
+
const harness: Harness = setUp({
|
|
1127
|
+
cluster: makeCluster({ isAiInvestigationEnabled: false }),
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
await register({ posture: { allowWrites: true } });
|
|
1131
|
+
|
|
1132
|
+
expect(defaultsWrite(harness).data).not.toHaveProperty(
|
|
1133
|
+
"aiAccessConfiguredAt",
|
|
1134
|
+
);
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
test("a read-only agent never turns fixes on", async () => {
|
|
1138
|
+
const harness: Harness = setUp({
|
|
1139
|
+
cluster: makeCluster({ isAiInvestigationEnabled: false }),
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
await register({ posture: { allowWrites: false } });
|
|
1143
|
+
|
|
1144
|
+
expect(defaultsWrite(harness).data).not.toHaveProperty("aiRemediationMode");
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
test.each([
|
|
1148
|
+
KubernetesAiRemediationMode.RequireApproval,
|
|
1149
|
+
KubernetesAiRemediationMode.Automatic,
|
|
1150
|
+
KubernetesAiRemediationMode.BypassApproval,
|
|
1151
|
+
])(
|
|
1152
|
+
"a mode already on the cluster (%s) is never changed",
|
|
1153
|
+
async (mode: KubernetesAiRemediationMode) => {
|
|
1154
|
+
const harness: Harness = setUp({
|
|
1155
|
+
cluster: makeCluster({ aiRemediationMode: mode }),
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
await register({ posture: { allowWrites: true } });
|
|
1159
|
+
|
|
1160
|
+
expect(harness.clusterUpdateOneBy.mock.calls).toHaveLength(0);
|
|
1161
|
+
},
|
|
1162
|
+
);
|
|
1163
|
+
|
|
1164
|
+
test("an unknown stored mode reads as Off", async () => {
|
|
1165
|
+
const harness: Harness = setUp({
|
|
1166
|
+
cluster: makeCluster({
|
|
1167
|
+
aiRemediationMode: "Sometimes" as KubernetesAiRemediationMode,
|
|
1168
|
+
}),
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
await register({ posture: { allowWrites: true } });
|
|
1172
|
+
|
|
1173
|
+
expect(defaultsWrite(harness).data["aiRemediationMode"]).toBe(
|
|
1174
|
+
KubernetesAiRemediationMode.RequireApproval,
|
|
1175
|
+
);
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
test("nothing is written when nothing needs changing", async () => {
|
|
1179
|
+
const harness: Harness = setUp();
|
|
1180
|
+
|
|
1181
|
+
await register();
|
|
1182
|
+
|
|
1183
|
+
expect(harness.clusterUpdateOneBy.mock.calls).toHaveLength(0);
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
test("a cluster someone configured is left exactly as chosen — investigation off and fixes Off included", async () => {
|
|
1187
|
+
const harness: Harness = setUp({
|
|
1188
|
+
cluster: makeCluster({
|
|
1189
|
+
aiAccessConfiguredAt: msAgo(60 * 1000),
|
|
1190
|
+
isAiInvestigationEnabled: false,
|
|
1191
|
+
aiRemediationMode: KubernetesAiRemediationMode.Disabled,
|
|
1192
|
+
}),
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
await register({ posture: { allowWrites: true } });
|
|
1196
|
+
|
|
1197
|
+
expect(harness.clusterUpdateOneBy.mock.calls).toHaveLength(0);
|
|
1198
|
+
expect(harness.clusterUpdateOneById.mock.calls).toHaveLength(0);
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
test("they apply on a re-key too, with a feed item saying what changed", async () => {
|
|
1202
|
+
const harness: Harness = setUp({
|
|
1203
|
+
existing: makeAgent({ connectionStatus: "disconnected" }),
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
await register({
|
|
1207
|
+
posture: { allowWrites: true, writeNamespaces: ["web", "api"] },
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
expect(defaultsWrite(harness).data).toEqual({
|
|
1211
|
+
aiRemediationMode: KubernetesAiRemediationMode.RequireApproval,
|
|
1212
|
+
});
|
|
1213
|
+
expect(harness.feed.mock.calls).toHaveLength(1);
|
|
1214
|
+
|
|
1215
|
+
const item: Record<string, unknown> = harness.feed.mock
|
|
1216
|
+
.calls[0]![0] as Record<string, unknown>;
|
|
1217
|
+
expect(String(item["feedInfoInMarkdown"])).toContain("Ask for approval");
|
|
1218
|
+
expect(String(item["feedInfoInMarkdown"])).toContain("in web, api");
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
test("when an operator configured the cluster a moment earlier, the conditional update lands nowhere and nothing is claimed", async () => {
|
|
1222
|
+
const harness: Harness = setUp({
|
|
1223
|
+
existing: makeAgent({ connectionStatus: "disconnected" }),
|
|
1224
|
+
cluster: makeCluster({ isAiInvestigationEnabled: false }),
|
|
1225
|
+
clusterUpdateCount: 0,
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
await register();
|
|
1229
|
+
|
|
1230
|
+
expect(harness.clusterUpdateOneBy.mock.calls).toHaveLength(1);
|
|
1231
|
+
expect(harness.feed.mock.calls).toHaveLength(0);
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
test("they never apply to a refused registration", async () => {
|
|
1235
|
+
const harness: Harness = setUp({
|
|
1236
|
+
existing: makeAgent(),
|
|
1237
|
+
cluster: makeCluster({ isAiInvestigationEnabled: false }),
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
await refusalOf(register({ posture: { allowWrites: true } }));
|
|
1241
|
+
|
|
1242
|
+
expect(harness.clusterUpdateOneBy.mock.calls).toHaveLength(0);
|
|
1243
|
+
});
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
/*
|
|
1247
|
+
* The key's hash is written before the defaults. If a failed defaults write
|
|
1248
|
+
* failed the registration, the agent would never receive the key its row
|
|
1249
|
+
* now demands, and — the row reading online — every retry would be refused
|
|
1250
|
+
* as previous_instance_online (and recorded as another agent's attempt)
|
|
1251
|
+
* until the alive window lapsed.
|
|
1252
|
+
*/
|
|
1253
|
+
describe("register: when applying the defaults fails", () => {
|
|
1254
|
+
function failTheDefaults(harness: Harness): void {
|
|
1255
|
+
(
|
|
1256
|
+
harness.clusterUpdateOneBy as unknown as {
|
|
1257
|
+
mockRejectedValue: (error: Error) => void;
|
|
1258
|
+
}
|
|
1259
|
+
).mockRejectedValue(new Error("database is down"));
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
test("a new agent still gets its key, and the row stores that key's hash", async () => {
|
|
1263
|
+
const harness: Harness = setUp({
|
|
1264
|
+
cluster: makeCluster({ isAiInvestigationEnabled: false }),
|
|
1265
|
+
});
|
|
1266
|
+
failTheDefaults(harness);
|
|
1267
|
+
|
|
1268
|
+
const result: KubernetesAiAgentRegistrationResult = await register();
|
|
1269
|
+
|
|
1270
|
+
expect(harness.clusterUpdateOneBy.mock.calls).toHaveLength(1);
|
|
1271
|
+
expect(result.admission).toBe("created");
|
|
1272
|
+
expect(result.agentId.toString()).toBe(AGENT_ID.toString());
|
|
1273
|
+
expect(createdRow(harness).keyHash).toBe(
|
|
1274
|
+
KubernetesAiAgentServiceClass.hashKey(result.agentKey),
|
|
1275
|
+
);
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
test("a re-key still hands out its key, and the next registration with it proves continuity while the row reads online", async () => {
|
|
1279
|
+
const harness: Harness = setUp({
|
|
1280
|
+
existing: makeAgent({ connectionStatus: "disconnected" }),
|
|
1281
|
+
cluster: makeCluster({ isAiInvestigationEnabled: false }),
|
|
1282
|
+
});
|
|
1283
|
+
failTheDefaults(harness);
|
|
1284
|
+
|
|
1285
|
+
const result: KubernetesAiAgentRegistrationResult = await register();
|
|
1286
|
+
|
|
1287
|
+
const storedHash: unknown = reKeyWrite(harness).data["keyHash"];
|
|
1288
|
+
expect(storedHash).toBe(
|
|
1289
|
+
KubernetesAiAgentServiceClass.hashKey(result.agentKey),
|
|
1290
|
+
);
|
|
1291
|
+
|
|
1292
|
+
// The row as the failed request left it: connected, alive just now.
|
|
1293
|
+
const rowAfter: KubernetesAiAgent = makeAgent({
|
|
1294
|
+
keyHash: storedHash,
|
|
1295
|
+
connectionStatus: "connected",
|
|
1296
|
+
lastAliveAt: new Date(),
|
|
1297
|
+
});
|
|
1298
|
+
expect(KubernetesAiAgentService.isOnline(rowAfter)).toBe(true);
|
|
1299
|
+
expect(
|
|
1300
|
+
KubernetesAiAgentService.getReRegistrationAdmission({
|
|
1301
|
+
agent: rowAfter,
|
|
1302
|
+
previousAgentKey: result.agentKey,
|
|
1303
|
+
}),
|
|
1304
|
+
).toBe("continuity");
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
test("the connected feed item says only what is true: investigation stayed off, and fixes were not changed", async () => {
|
|
1308
|
+
const harness: Harness = setUp({
|
|
1309
|
+
cluster: makeCluster({ isAiInvestigationEnabled: false }),
|
|
1310
|
+
});
|
|
1311
|
+
failTheDefaults(harness);
|
|
1312
|
+
|
|
1313
|
+
await register({ posture: { allowWrites: true } });
|
|
1314
|
+
|
|
1315
|
+
expect(harness.feed.mock.calls).toHaveLength(1);
|
|
1316
|
+
const text: string = String(
|
|
1317
|
+
(harness.feed.mock.calls[0]![0] as Record<string, unknown>)[
|
|
1318
|
+
"feedInfoInMarkdown"
|
|
1319
|
+
],
|
|
1320
|
+
);
|
|
1321
|
+
expect(text).toContain("The Kubernetes AI agent connected");
|
|
1322
|
+
expect(text).toContain("Investigating with kubectl is off");
|
|
1323
|
+
expect(text).not.toContain("Ask for approval");
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
test("a re-key writes no defaults feed item", async () => {
|
|
1327
|
+
const harness: Harness = setUp({
|
|
1328
|
+
existing: makeAgent({ connectionStatus: "disconnected" }),
|
|
1329
|
+
cluster: makeCluster({ isAiInvestigationEnabled: false }),
|
|
1330
|
+
});
|
|
1331
|
+
failTheDefaults(harness);
|
|
1332
|
+
|
|
1333
|
+
await register({ posture: { allowWrites: true } });
|
|
1334
|
+
|
|
1335
|
+
expect(harness.feed.mock.calls).toHaveLength(0);
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
test("the failure is logged, naming the cluster", async () => {
|
|
1339
|
+
const harness: Harness = setUp({
|
|
1340
|
+
cluster: makeCluster({ isAiInvestigationEnabled: false }),
|
|
1341
|
+
});
|
|
1342
|
+
failTheDefaults(harness);
|
|
1343
|
+
|
|
1344
|
+
await register();
|
|
1345
|
+
|
|
1346
|
+
const logged: string = JSON.stringify(
|
|
1347
|
+
(logger.error as unknown as SpyCalls).mock.calls,
|
|
1348
|
+
);
|
|
1349
|
+
expect(logged).toContain("first-connection defaults");
|
|
1350
|
+
expect(logged).toContain(CLUSTER_ID.toString());
|
|
1351
|
+
expect(logged).toContain("database is down");
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
test("a failure before the key is stored still fails the registration", async () => {
|
|
1355
|
+
const harness: Harness = setUp({
|
|
1356
|
+
existing: makeAgent({ connectionStatus: "disconnected" }),
|
|
1357
|
+
});
|
|
1358
|
+
(
|
|
1359
|
+
harness.agentUpdateOneById as unknown as {
|
|
1360
|
+
mockRejectedValue: (error: Error) => void;
|
|
1361
|
+
}
|
|
1362
|
+
).mockRejectedValue(new Error("database is down"));
|
|
1363
|
+
|
|
1364
|
+
await expect(register()).rejects.toThrow("database is down");
|
|
1365
|
+
expect(harness.clusterUpdateOneBy.mock.calls).toHaveLength(0);
|
|
1366
|
+
});
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
describe("register: the cluster feed", () => {
|
|
1370
|
+
function feedItem(harness: Harness): Record<string, unknown> {
|
|
1371
|
+
expect(harness.feed.mock.calls).toHaveLength(1);
|
|
1372
|
+
return harness.feed.mock.calls[0]![0] as Record<string, unknown>;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
test("one item marks the first connection of a cluster's agent", async () => {
|
|
1376
|
+
const harness: Harness = setUp();
|
|
1377
|
+
|
|
1378
|
+
await register();
|
|
1379
|
+
|
|
1380
|
+
const item: Record<string, unknown> = feedItem(harness);
|
|
1381
|
+
expect(String(item["kubernetesClusterId"])).toBe(CLUSTER_ID.toString());
|
|
1382
|
+
expect(item["projectId"]).toBe(PROJECT_ID);
|
|
1383
|
+
expect(String(item["feedInfoInMarkdown"])).toContain(
|
|
1384
|
+
"The Kubernetes AI agent connected (read-only).",
|
|
1385
|
+
);
|
|
1386
|
+
expect(String(item["feedInfoInMarkdown"])).toContain(
|
|
1387
|
+
"AI can now use it to investigate this cluster.",
|
|
1388
|
+
);
|
|
1389
|
+
expect(String(item["moreInformationInMarkdown"])).toContain(
|
|
1390
|
+
AGENT_ID.toString(),
|
|
1391
|
+
);
|
|
1392
|
+
expect(String(item["moreInformationInMarkdown"])).toContain("v1.33.1");
|
|
1393
|
+
});
|
|
1394
|
+
|
|
1395
|
+
test("says where a writing agent may change things", async () => {
|
|
1396
|
+
const scoped: Harness = setUp();
|
|
1397
|
+
await register({
|
|
1398
|
+
posture: { allowWrites: true, writeNamespaces: ["web", "api"] },
|
|
1399
|
+
});
|
|
1400
|
+
expect(String(feedItem(scoped)["feedInfoInMarkdown"])).toContain(
|
|
1401
|
+
"(can make changes in web, api)",
|
|
1402
|
+
);
|
|
1403
|
+
jest.restoreAllMocks();
|
|
1404
|
+
|
|
1405
|
+
const clusterWide: Harness = setUp();
|
|
1406
|
+
await register({ posture: { allowWrites: true, writeNamespaces: [] } });
|
|
1407
|
+
expect(String(feedItem(clusterWide)["feedInfoInMarkdown"])).toContain(
|
|
1408
|
+
"(can make changes anywhere in the cluster)",
|
|
1409
|
+
);
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
test("says fixes were set to Ask for approval when the defaults did that", async () => {
|
|
1413
|
+
const harness: Harness = setUp();
|
|
1414
|
+
|
|
1415
|
+
await register({ posture: { allowWrites: true } });
|
|
1416
|
+
|
|
1417
|
+
expect(String(feedItem(harness)["feedInfoInMarkdown"])).toContain(
|
|
1418
|
+
'"Ask for approval"',
|
|
1419
|
+
);
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
test("on a cluster where an operator turned investigation off, says so rather than promising investigations", async () => {
|
|
1423
|
+
const harness: Harness = setUp({
|
|
1424
|
+
cluster: makeCluster({
|
|
1425
|
+
aiAccessConfiguredAt: msAgo(60 * 1000),
|
|
1426
|
+
isAiInvestigationEnabled: false,
|
|
1427
|
+
}),
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
await register();
|
|
1431
|
+
|
|
1432
|
+
const text: string = String(feedItem(harness)["feedInfoInMarkdown"]);
|
|
1433
|
+
expect(text).toContain("Investigating with kubectl is off");
|
|
1434
|
+
expect(text).not.toContain("AI can now use it");
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
test("never carries the agent key", async () => {
|
|
1438
|
+
const harness: Harness = setUp();
|
|
1439
|
+
|
|
1440
|
+
const result: KubernetesAiAgentRegistrationResult = await register();
|
|
1441
|
+
|
|
1442
|
+
expect(JSON.stringify(harness.feed.mock.calls)).not.toContain(
|
|
1443
|
+
result.agentKey,
|
|
1444
|
+
);
|
|
1445
|
+
});
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
describe("getReRegistrationAdmission", () => {
|
|
1449
|
+
const NOW: Date = new Date("2026-09-28T10:00:00.000Z");
|
|
1450
|
+
const HASH: string = KubernetesAiAgentServiceClass.hashKey(CURRENT_KEY);
|
|
1451
|
+
|
|
1452
|
+
function admission(
|
|
1453
|
+
agent: Record<string, unknown>,
|
|
1454
|
+
previousAgentKey?: string,
|
|
1455
|
+
): string | null {
|
|
1456
|
+
return KubernetesAiAgentService.getReRegistrationAdmission({
|
|
1457
|
+
agent: agent as never,
|
|
1458
|
+
previousAgentKey,
|
|
1459
|
+
now: NOW,
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
const ONLINE: Record<string, unknown> = {
|
|
1464
|
+
keyHash: HASH,
|
|
1465
|
+
connectionStatus: "connected",
|
|
1466
|
+
lastAliveAt: new Date(NOW.getTime() - 30 * 1000),
|
|
1467
|
+
};
|
|
1468
|
+
|
|
1469
|
+
test("reset beats everything", () => {
|
|
1470
|
+
expect(admission({ ...ONLINE, keyHash: null })).toBe("reset");
|
|
1471
|
+
expect(admission({ ...ONLINE, keyHash: "" })).toBe("reset");
|
|
1472
|
+
expect(admission({ ...ONLINE, keyHash: undefined })).toBe("reset");
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
test("continuity needs the current key itself", () => {
|
|
1476
|
+
expect(admission(ONLINE, CURRENT_KEY)).toBe("continuity");
|
|
1477
|
+
expect(admission(ONLINE, "cd".repeat(32))).toBeNull();
|
|
1478
|
+
expect(admission(ONLINE, HASH)).toBeNull();
|
|
1479
|
+
expect(admission(ONLINE, "")).toBeNull();
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
test("signed off, then offline, then refused", () => {
|
|
1483
|
+
expect(admission({ ...ONLINE, connectionStatus: "disconnected" })).toBe(
|
|
1484
|
+
"signed_off",
|
|
1485
|
+
);
|
|
1486
|
+
expect(
|
|
1487
|
+
admission({
|
|
1488
|
+
...ONLINE,
|
|
1489
|
+
lastAliveAt: new Date(NOW.getTime() - ALIVE_WINDOW_MS - 1000),
|
|
1490
|
+
}),
|
|
1491
|
+
).toBe("offline");
|
|
1492
|
+
expect(admission({ ...ONLINE, lastAliveAt: null })).toBe("offline");
|
|
1493
|
+
expect(admission(ONLINE)).toBeNull();
|
|
1494
|
+
});
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
describe("getFirstConnectionDefaults", () => {
|
|
1498
|
+
function defaults(
|
|
1499
|
+
cluster: Partial<KubernetesCluster>,
|
|
1500
|
+
allowWrites: boolean,
|
|
1501
|
+
): Record<string, unknown> {
|
|
1502
|
+
return KubernetesAiAgentService.getFirstConnectionDefaults({
|
|
1503
|
+
cluster: cluster as KubernetesCluster,
|
|
1504
|
+
allowWrites,
|
|
1505
|
+
}) as unknown as Record<string, unknown>;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
test("configured clusters get nothing", () => {
|
|
1509
|
+
expect(
|
|
1510
|
+
defaults(
|
|
1511
|
+
{
|
|
1512
|
+
aiAccessConfiguredAt: new Date(),
|
|
1513
|
+
isAiInvestigationEnabled: false,
|
|
1514
|
+
aiRemediationMode: KubernetesAiRemediationMode.Disabled,
|
|
1515
|
+
},
|
|
1516
|
+
true,
|
|
1517
|
+
),
|
|
1518
|
+
).toEqual({ turnedOnInvestigation: false });
|
|
1519
|
+
});
|
|
1520
|
+
|
|
1521
|
+
test("unconfigured: investigation on when it is not, fixes Ask for approval only with writes and only from Off", () => {
|
|
1522
|
+
expect(defaults({ isAiInvestigationEnabled: false }, false)).toEqual({
|
|
1523
|
+
turnedOnInvestigation: true,
|
|
1524
|
+
});
|
|
1525
|
+
expect(defaults({ isAiInvestigationEnabled: true }, false)).toEqual({
|
|
1526
|
+
turnedOnInvestigation: false,
|
|
1527
|
+
});
|
|
1528
|
+
expect(
|
|
1529
|
+
defaults(
|
|
1530
|
+
{
|
|
1531
|
+
isAiInvestigationEnabled: true,
|
|
1532
|
+
aiRemediationMode: KubernetesAiRemediationMode.Disabled,
|
|
1533
|
+
},
|
|
1534
|
+
true,
|
|
1535
|
+
),
|
|
1536
|
+
).toEqual({
|
|
1537
|
+
turnedOnInvestigation: false,
|
|
1538
|
+
remediationMode: KubernetesAiRemediationMode.RequireApproval,
|
|
1539
|
+
});
|
|
1540
|
+
expect(
|
|
1541
|
+
defaults(
|
|
1542
|
+
{
|
|
1543
|
+
isAiInvestigationEnabled: true,
|
|
1544
|
+
aiRemediationMode: KubernetesAiRemediationMode.Automatic,
|
|
1545
|
+
},
|
|
1546
|
+
true,
|
|
1547
|
+
),
|
|
1548
|
+
).toEqual({ turnedOnInvestigation: false });
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
test("didApplyDefaults is true only when something changes", () => {
|
|
1552
|
+
expect(
|
|
1553
|
+
KubernetesAiAgentServiceClass.didApplyDefaults({
|
|
1554
|
+
turnedOnInvestigation: false,
|
|
1555
|
+
}),
|
|
1556
|
+
).toBe(false);
|
|
1557
|
+
expect(
|
|
1558
|
+
KubernetesAiAgentServiceClass.didApplyDefaults({
|
|
1559
|
+
turnedOnInvestigation: true,
|
|
1560
|
+
}),
|
|
1561
|
+
).toBe(true);
|
|
1562
|
+
expect(
|
|
1563
|
+
KubernetesAiAgentServiceClass.didApplyDefaults({
|
|
1564
|
+
turnedOnInvestigation: false,
|
|
1565
|
+
remediationMode: KubernetesAiRemediationMode.RequireApproval,
|
|
1566
|
+
}),
|
|
1567
|
+
).toBe(true);
|
|
1568
|
+
});
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
describe("buildStoredPosture and the wording helpers", () => {
|
|
1572
|
+
test("buildStoredPosture drops unknown keys and never stores undefined", () => {
|
|
1573
|
+
const posture: KubernetesAgentPosture =
|
|
1574
|
+
KubernetesAiAgentService.buildStoredPosture({
|
|
1575
|
+
reported: { allowWrites: true, extra: "ignored" },
|
|
1576
|
+
clusterIdentifier: " prod-us ",
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1579
|
+
expect(posture).toEqual({
|
|
1580
|
+
clusterIdentifier: "prod-us",
|
|
1581
|
+
inCluster: true,
|
|
1582
|
+
allowWrites: true,
|
|
1583
|
+
});
|
|
1584
|
+
expect(Object.values(posture)).not.toContain(undefined);
|
|
1585
|
+
});
|
|
1586
|
+
|
|
1587
|
+
test("an empty cluster identifier is left out rather than stored blank", () => {
|
|
1588
|
+
expect(
|
|
1589
|
+
KubernetesAiAgentService.buildStoredPosture({
|
|
1590
|
+
reported: {},
|
|
1591
|
+
clusterIdentifier: "",
|
|
1592
|
+
}),
|
|
1593
|
+
).toEqual({ inCluster: true, allowWrites: false });
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
test("describeWriteAccess", () => {
|
|
1597
|
+
expect(
|
|
1598
|
+
KubernetesAiAgentServiceClass.describeWriteAccess({ allowWrites: false }),
|
|
1599
|
+
).toBe("read-only");
|
|
1600
|
+
expect(KubernetesAiAgentServiceClass.describeWriteAccess({})).toBe(
|
|
1601
|
+
"read-only",
|
|
1602
|
+
);
|
|
1603
|
+
expect(
|
|
1604
|
+
KubernetesAiAgentServiceClass.describeWriteAccess({
|
|
1605
|
+
allowWrites: true,
|
|
1606
|
+
writeNamespaces: ["web"],
|
|
1607
|
+
}),
|
|
1608
|
+
).toBe("can make changes in web");
|
|
1609
|
+
expect(
|
|
1610
|
+
KubernetesAiAgentServiceClass.describeWriteAccess({
|
|
1611
|
+
allowWrites: true,
|
|
1612
|
+
writeNamespaces: [],
|
|
1613
|
+
}),
|
|
1614
|
+
).toBe("can make changes anywhere in the cluster");
|
|
1615
|
+
expect(
|
|
1616
|
+
KubernetesAiAgentServiceClass.describeWriteAccess({ allowWrites: true }),
|
|
1617
|
+
).toBe("can make changes anywhere in the cluster");
|
|
1618
|
+
});
|
|
1619
|
+
|
|
1620
|
+
test("normalizeAgentVersion", () => {
|
|
1621
|
+
expect(KubernetesAiAgentServiceClass.normalizeAgentVersion(" 1.2 ")).toBe(
|
|
1622
|
+
"1.2",
|
|
1623
|
+
);
|
|
1624
|
+
expect(
|
|
1625
|
+
KubernetesAiAgentServiceClass.normalizeAgentVersion(" "),
|
|
1626
|
+
).toBeUndefined();
|
|
1627
|
+
expect(
|
|
1628
|
+
KubernetesAiAgentServiceClass.normalizeAgentVersion(12),
|
|
1629
|
+
).toBeUndefined();
|
|
1630
|
+
expect(
|
|
1631
|
+
KubernetesAiAgentServiceClass.normalizeAgentVersion("x".repeat(101)),
|
|
1632
|
+
).toHaveLength(ColumnLength.ShortText);
|
|
1633
|
+
});
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1636
|
+
describe("KubernetesAiAgentRegistrationRefusedException", () => {
|
|
1637
|
+
test("is a 403 carrying the reason and, when given, when to retry", () => {
|
|
1638
|
+
const reasons: Array<KubernetesAiAgentRegistrationRefusalReason> = [
|
|
1639
|
+
"previous_instance_online",
|
|
1640
|
+
"legacy_runner_online",
|
|
1641
|
+
"cluster_name_invalid",
|
|
1642
|
+
"agent_cap_reached",
|
|
1643
|
+
];
|
|
1644
|
+
|
|
1645
|
+
for (const reason of reasons) {
|
|
1646
|
+
const refusal: KubernetesAiAgentRegistrationRefusedException =
|
|
1647
|
+
new KubernetesAiAgentRegistrationRefusedException({
|
|
1648
|
+
reason,
|
|
1649
|
+
message: "m",
|
|
1650
|
+
...(isTransientKubernetesAiAgentRegistrationRefusal(reason)
|
|
1651
|
+
? { retryAfterSeconds: 20 }
|
|
1652
|
+
: {}),
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
expect(refusal).toBeInstanceOf(ForbiddenException);
|
|
1656
|
+
expect(refusal.code).toBe(403);
|
|
1657
|
+
expect(refusal.reason).toBe(reason);
|
|
1658
|
+
expect(refusal.message).toBe("m");
|
|
1659
|
+
expect(refusal.retryAfterSeconds).toBe(
|
|
1660
|
+
isTransientKubernetesAiAgentRegistrationRefusal(reason)
|
|
1661
|
+
? 20
|
|
1662
|
+
: undefined,
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1665
|
+
});
|
|
1666
|
+
});
|