From 087ea54e99e1653dda3fc08cb84f0388b6cbf27c Mon Sep 17 00:00:00 2001 From: Adarsh Kumar Yadav Date: Mon, 18 May 2026 18:41:21 +0530 Subject: [PATCH 1/6] feat(controller): emit events for MCP handshake failures Emit Warning events when the MCP handshake fails (deduplicated by condition message) and when handshake retries are exhausted. Part of #109. --- internal/controller/mcpserver_controller.go | 37 +++++ .../mcpserver_controller_conditions_test.go | 14 ++ .../mcpserver_controller_handshake_test.go | 129 ++++++++++++++++++ 3 files changed, 180 insertions(+) diff --git a/internal/controller/mcpserver_controller.go b/internal/controller/mcpserver_controller.go index caae202b..2dc1c688 100644 --- a/internal/controller/mcpserver_controller.go +++ b/internal/controller/mcpserver_controller.go @@ -131,6 +131,10 @@ const ( eventActionConfigurationAccepted = "ConfigurationAccepted" // eventActionServerReady is the reporting action when Ready becomes True with reason Available. eventActionServerReady = "ServerReady" + // eventActionMCPHandshakeFailed is the reporting action when the MCP handshake fails. + eventActionMCPHandshakeFailed = "MCPHandshakeFailed" + // eventActionMCPHandshakeRetriesExhausted is the reporting action when handshake retries are exhausted. + eventActionMCPHandshakeRetriesExhausted = "MCPHandshakeRetriesExhausted" // requeueDelayMCPHandshake is the initial delay before requeuing when an MCP handshake fails. requeueDelayMCPHandshake = 10 * time.Second @@ -356,12 +360,23 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( } var handshakeRetryCount int32 + prevHandshakeRetryCount := mcpServer.Status.HandshakeRetryCount + if mcpServer.Status.ObservedGeneration != mcpServer.Generation { + prevHandshakeRetryCount = 0 + } if readyCondition.Reason == ReasonMCPEndpointUnavailable { if mcpServer.Status.ObservedGeneration == mcpServer.Generation { handshakeRetryCount = mcpServer.Status.HandshakeRetryCount + 1 } else { handshakeRetryCount = 1 } + + if !duplicateHandshakeUnavailable(mcpServer.Status.Conditions, readyCondition.Message) { + r.emitMCPHandshakeFailed(mcpServer, readyCondition.Message) + } + if int(handshakeRetryCount) >= maxMCPHandshakeRetries && int(prevHandshakeRetryCount) < maxMCPHandshakeRetries { + r.emitMCPHandshakeRetriesExhausted(mcpServer, handshakeRetryCount) + } } status := acv1alpha1.MCPServerStatus(). @@ -1256,6 +1271,12 @@ func readyConditionIsAvailable(conditions []metav1.Condition) bool { return c != nil && c.Status == metav1.ConditionTrue && c.Reason == ReasonAvailable } +func duplicateHandshakeUnavailable(conditions []metav1.Condition, message string) bool { + prevReady := meta.FindStatusCondition(conditions, ConditionTypeReady) + return prevReady != nil && prevReady.Status == metav1.ConditionFalse && + prevReady.Reason == ReasonMCPEndpointUnavailable && prevReady.Message == message +} + func (r *MCPServerReconciler) reconcilePermanentValidationError( ctx context.Context, mcpServer *mcpv1alpha1.MCPServer, @@ -1339,6 +1360,22 @@ func (r *MCPServerReconciler) emitServerReady(mcpServer *mcpv1alpha1.MCPServer) r.Recorder.Eventf(mcpServer, nil, corev1.EventTypeNormal, ReasonAvailable, eventActionServerReady, "MCPServer %s is ready; Ready=True", mcpServer.Name) } +func (r *MCPServerReconciler) emitMCPHandshakeFailed(mcpServer *mcpv1alpha1.MCPServer, message string) { + if r.Recorder == nil { + return + } + r.Recorder.Eventf(mcpServer, nil, corev1.EventTypeWarning, ReasonMCPEndpointUnavailable, eventActionMCPHandshakeFailed, "%s", message) +} + +func (r *MCPServerReconciler) emitMCPHandshakeRetriesExhausted(mcpServer *mcpv1alpha1.MCPServer, retryCount int32) { + if r.Recorder == nil { + return + } + r.Recorder.Eventf(mcpServer, nil, corev1.EventTypeWarning, ReasonMCPEndpointUnavailable, eventActionMCPHandshakeRetriesExhausted, + "MCP handshake retries exhausted for MCPServer %s after %d attempts; fix the MCP endpoint or update spec to retry", + mcpServer.Name, retryCount) +} + func (r *MCPServerReconciler) applyStatus( ctx context.Context, mcpServer *mcpv1alpha1.MCPServer, diff --git a/internal/controller/mcpserver_controller_conditions_test.go b/internal/controller/mcpserver_controller_conditions_test.go index ef9f2b15..c015d050 100644 --- a/internal/controller/mcpserver_controller_conditions_test.go +++ b/internal/controller/mcpserver_controller_conditions_test.go @@ -673,4 +673,18 @@ var _ = Describe("status condition helpers", func() { {Type: ConditionTypeReady, Status: metav1.ConditionTrue, Reason: ReasonAvailable}, })).To(BeTrue()) }) + + It("duplicateHandshakeUnavailable returns true only for matching Ready=False MCPEndpointUnavailable message", func() { + msg := "MCP endpoint is not serving a valid MCP protocol: connection refused" + Expect(duplicateHandshakeUnavailable(nil, msg)).To(BeFalse()) + Expect(duplicateHandshakeUnavailable([]metav1.Condition{ + {Type: ConditionTypeReady, Status: metav1.ConditionFalse, Reason: ReasonDeploymentUnavailable, Message: msg}, + }, msg)).To(BeFalse()) + Expect(duplicateHandshakeUnavailable([]metav1.Condition{ + {Type: ConditionTypeReady, Status: metav1.ConditionFalse, Reason: ReasonMCPEndpointUnavailable, Message: "other"}, + }, msg)).To(BeFalse()) + Expect(duplicateHandshakeUnavailable([]metav1.Condition{ + {Type: ConditionTypeReady, Status: metav1.ConditionFalse, Reason: ReasonMCPEndpointUnavailable, Message: msg}, + }, msg)).To(BeTrue()) + }) }) diff --git a/internal/controller/mcpserver_controller_handshake_test.go b/internal/controller/mcpserver_controller_handshake_test.go index 32cb1061..9b1eabec 100644 --- a/internal/controller/mcpserver_controller_handshake_test.go +++ b/internal/controller/mcpserver_controller_handshake_test.go @@ -19,6 +19,7 @@ package controller import ( "context" "fmt" + "strings" "time" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -449,6 +450,134 @@ var _ = Describe("MCPServer Controller - MCP Handshake Validation", func() { Consistently(fr.Events, 300*time.Millisecond, 20*time.Millisecond).ShouldNot(Receive()) }) + It("should emit a Warning MCPHandshakeFailed event only when handshake error message changes", func() { + failMsg := "intentional failure" + reconciler, fr := newReconcilerForTestWithFakeEvents(k8sClient, k8sClient.Scheme()) + reconciler.MCPDialer = func(ctx context.Context, url string) (*mcpv1alpha1.MCPServerInfo, error) { + return nil, fmt.Errorf("%s", failMsg) + } + + By("Initial reconciliation creates deployment") + _, err := reconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + drainFakeRecorderEvents(fr) + + By("Simulating deployment becoming available") + deployment := &appsv1.Deployment{} + Expect(k8sClient.Get(ctx, client.ObjectKey{ + Name: resourceName, Namespace: "default", + }, deployment)).To(Succeed()) + deployment.Status.Replicas = 1 + deployment.Status.ReadyReplicas = 1 + deployment.Status.Conditions = []appsv1.DeploymentCondition{ + {Type: appsv1.DeploymentAvailable, Status: corev1.ConditionTrue}, + {Type: appsv1.DeploymentProgressing, Status: corev1.ConditionTrue}, + } + Expect(k8sClient.Status().Update(ctx, deployment)).To(Succeed()) + + By("First handshake failure — Warning event emitted once") + _, err = reconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + var handshakeFailedEvent string + Eventually(fr.Events).Should(Receive(&handshakeFailedEvent)) + Expect(handshakeFailedEvent).To(ContainSubstring(corev1.EventTypeWarning)) + Expect(handshakeFailedEvent).To(ContainSubstring(ReasonMCPEndpointUnavailable)) + Expect(handshakeFailedEvent).To(ContainSubstring(failMsg)) + + By("Second reconcile with same error — no duplicate handshake failed event") + _, err = reconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + Consistently(fr.Events, 300*time.Millisecond, 20*time.Millisecond).ShouldNot(Receive()) + }) + + It("should emit MCPHandshakeRetriesExhausted once when max handshake retries is reached", func() { + reconciler, fr := newReconcilerForTestWithFakeEvents(k8sClient, k8sClient.Scheme()) + reconciler.MCPDialer = func(ctx context.Context, url string) (*mcpv1alpha1.MCPServerInfo, error) { + return nil, fmt.Errorf("intentional failure") + } + + By("Initial reconciliation creates deployment") + _, err := reconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + drainFakeRecorderEvents(fr) + + By("Simulating deployment becoming available") + deployment := &appsv1.Deployment{} + Expect(k8sClient.Get(ctx, client.ObjectKey{ + Name: resourceName, Namespace: "default", + }, deployment)).To(Succeed()) + deployment.Status.Replicas = 1 + deployment.Status.ReadyReplicas = 1 + deployment.Status.Conditions = []appsv1.DeploymentCondition{ + {Type: appsv1.DeploymentAvailable, Status: corev1.ConditionTrue}, + {Type: appsv1.DeploymentProgressing, Status: corev1.ConditionTrue}, + } + Expect(k8sClient.Status().Update(ctx, deployment)).To(Succeed()) + + By("Reconciling until handshake retries are exhausted") + for i := 0; i < maxMCPHandshakeRetries; i++ { + result, recErr := reconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(recErr).NotTo(HaveOccurred()) + if i < maxMCPHandshakeRetries-1 { + Expect(result.RequeueAfter).To(BeNumerically(">", 0)) + } + } + + var collected []string + Eventually(func(g Gomega) { + for { + select { + case ev := <-fr.Events: + collected = append(collected, ev) + default: + goto check + } + } + check: + exhausted := 0 + for _, ev := range collected { + if strings.Contains(ev, "retries exhausted") { + exhausted++ + } + } + g.Expect(exhausted).To(Equal(1)) + }).Should(Succeed()) + var exhaustedEvent string + for _, ev := range collected { + if strings.Contains(ev, "retries exhausted") { + exhaustedEvent = ev + break + } + } + Expect(exhaustedEvent).To(ContainSubstring(corev1.EventTypeWarning)) + Expect(exhaustedEvent).To(ContainSubstring(ReasonMCPEndpointUnavailable)) + Expect(exhaustedEvent).To(ContainSubstring(resourceName)) + + mcpServer := &mcpv1alpha1.MCPServer{} + Expect(k8sClient.Get(ctx, typeNamespacedName, mcpServer)).To(Succeed()) + Expect(mcpServer.Status.HandshakeRetryCount).To(BeNumerically(">=", maxMCPHandshakeRetries)) + + By("Further reconcile — no duplicate exhausted event") + drainFakeRecorderEvents(fr) + result, err := reconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(result.RequeueAfter).To(BeZero()) + Consistently(fr.Events, 300*time.Millisecond, 20*time.Millisecond).ShouldNot(Receive()) + }) + It("should pass a context with timeout to the dialer", func() { var receivedCtx context.Context reconciler := &MCPServerReconciler{ From 5ec79f8a4db9153dd60b699f2994677d276e0c2f Mon Sep 17 00:00:00 2001 From: Adarsh Kumar Yadav Date: Mon, 18 May 2026 20:01:34 +0530 Subject: [PATCH 2/6] fix: satisfy golangci-lint gocyclo and modernize for handshake events Extract reconcileHandshakeEventsAndRetryCount to keep Reconcile under complexity 30; use range over int in handshake test. --- internal/controller/mcpserver_controller.go | 52 ++++++++++++------- .../mcpserver_controller_handshake_test.go | 2 +- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/internal/controller/mcpserver_controller.go b/internal/controller/mcpserver_controller.go index 2dc1c688..3bf4e65f 100644 --- a/internal/controller/mcpserver_controller.go +++ b/internal/controller/mcpserver_controller.go @@ -352,32 +352,13 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( var serverInfo *mcpv1alpha1.MCPServerInfo readyCondition, serverInfo = r.reconcileHandshake(ctx, mcpServer, mcpURL, readyCondition) - // Normal Event once per Ready transition to Available after a successful handshake. if pendingServerReadyEvent && readyCondition.Status == metav1.ConditionTrue && readyCondition.Reason == ReasonAvailable { r.emitServerReady(mcpServer) } - var handshakeRetryCount int32 - prevHandshakeRetryCount := mcpServer.Status.HandshakeRetryCount - if mcpServer.Status.ObservedGeneration != mcpServer.Generation { - prevHandshakeRetryCount = 0 - } - if readyCondition.Reason == ReasonMCPEndpointUnavailable { - if mcpServer.Status.ObservedGeneration == mcpServer.Generation { - handshakeRetryCount = mcpServer.Status.HandshakeRetryCount + 1 - } else { - handshakeRetryCount = 1 - } - - if !duplicateHandshakeUnavailable(mcpServer.Status.Conditions, readyCondition.Message) { - r.emitMCPHandshakeFailed(mcpServer, readyCondition.Message) - } - if int(handshakeRetryCount) >= maxMCPHandshakeRetries && int(prevHandshakeRetryCount) < maxMCPHandshakeRetries { - r.emitMCPHandshakeRetriesExhausted(mcpServer, handshakeRetryCount) - } - } + handshakeRetryCount := r.reconcileHandshakeEventsAndRetryCount(mcpServer, readyCondition) status := acv1alpha1.MCPServerStatus(). WithObservedGeneration(mcpServer.Generation). @@ -450,6 +431,37 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( return ctrl.Result{}, nil } +// reconcileHandshakeEventsAndRetryCount emits handshake-related events and returns the updated retry count. +func (r *MCPServerReconciler) reconcileHandshakeEventsAndRetryCount( + mcpServer *mcpv1alpha1.MCPServer, + readyCondition metav1.Condition, +) int32 { + if readyCondition.Reason != ReasonMCPEndpointUnavailable { + return 0 + } + + prevHandshakeRetryCount := mcpServer.Status.HandshakeRetryCount + if mcpServer.Status.ObservedGeneration != mcpServer.Generation { + prevHandshakeRetryCount = 0 + } + + var handshakeRetryCount int32 + if mcpServer.Status.ObservedGeneration == mcpServer.Generation { + handshakeRetryCount = mcpServer.Status.HandshakeRetryCount + 1 + } else { + handshakeRetryCount = 1 + } + + if !duplicateHandshakeUnavailable(mcpServer.Status.Conditions, readyCondition.Message) { + r.emitMCPHandshakeFailed(mcpServer, readyCondition.Message) + } + if int(handshakeRetryCount) >= maxMCPHandshakeRetries && int(prevHandshakeRetryCount) < maxMCPHandshakeRetries { + r.emitMCPHandshakeRetriesExhausted(mcpServer, handshakeRetryCount) + } + + return handshakeRetryCount +} + // reconcileHandshake performs the MCP handshake when the deployment is available, // skipping it when the endpoint was already verified for the current generation. func (r *MCPServerReconciler) reconcileHandshake( diff --git a/internal/controller/mcpserver_controller_handshake_test.go b/internal/controller/mcpserver_controller_handshake_test.go index 9b1eabec..29094e21 100644 --- a/internal/controller/mcpserver_controller_handshake_test.go +++ b/internal/controller/mcpserver_controller_handshake_test.go @@ -524,7 +524,7 @@ var _ = Describe("MCPServer Controller - MCP Handshake Validation", func() { Expect(k8sClient.Status().Update(ctx, deployment)).To(Succeed()) By("Reconciling until handshake retries are exhausted") - for i := 0; i < maxMCPHandshakeRetries; i++ { + for i := range maxMCPHandshakeRetries { result, recErr := reconciler.Reconcile(ctx, reconcile.Request{ NamespacedName: typeNamespacedName, }) From 5fca5f76c4853be6016b8c44da8a3d4e26741ec0 Mon Sep 17 00:00:00 2001 From: Adarsh Kumar Yadav Date: Wed, 20 May 2026 10:26:19 +0530 Subject: [PATCH 3/6] fix(controller): include MCPServer name in all emitted event messages Address review feedback on #186 and align event messages with emitServerReady and emitMCPHandshakeRetriesExhausted. --- internal/controller/mcpserver_controller.go | 9 ++++++--- .../controller/mcpserver_controller_handshake_test.go | 1 + internal/controller/mcpserver_controller_test.go | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/internal/controller/mcpserver_controller.go b/internal/controller/mcpserver_controller.go index 3bf4e65f..0a54b190 100644 --- a/internal/controller/mcpserver_controller.go +++ b/internal/controller/mcpserver_controller.go @@ -1355,14 +1355,16 @@ func (r *MCPServerReconciler) emitConfigurationInvalid(mcpServer *mcpv1alpha1.MC if r.Recorder == nil { return } - r.Recorder.Eventf(mcpServer, nil, corev1.EventTypeWarning, validationErr.Reason, eventActionConfigurationValidation, "%s", validationErr.Message) + r.Recorder.Eventf(mcpServer, nil, corev1.EventTypeWarning, validationErr.Reason, eventActionConfigurationValidation, + "MCPServer %s: %s", mcpServer.Name, validationErr.Message) } func (r *MCPServerReconciler) emitConfigurationAccepted(mcpServer *mcpv1alpha1.MCPServer) { if r.Recorder == nil { return } - r.Recorder.Eventf(mcpServer, nil, corev1.EventTypeNormal, ReasonValid, eventActionConfigurationAccepted, "%s", "MCPServer configuration is valid; Accepted=True") + r.Recorder.Eventf(mcpServer, nil, corev1.EventTypeNormal, ReasonValid, eventActionConfigurationAccepted, + "MCPServer %s configuration is valid; Accepted=True", mcpServer.Name) } func (r *MCPServerReconciler) emitServerReady(mcpServer *mcpv1alpha1.MCPServer) { @@ -1376,7 +1378,8 @@ func (r *MCPServerReconciler) emitMCPHandshakeFailed(mcpServer *mcpv1alpha1.MCPS if r.Recorder == nil { return } - r.Recorder.Eventf(mcpServer, nil, corev1.EventTypeWarning, ReasonMCPEndpointUnavailable, eventActionMCPHandshakeFailed, "%s", message) + r.Recorder.Eventf(mcpServer, nil, corev1.EventTypeWarning, ReasonMCPEndpointUnavailable, eventActionMCPHandshakeFailed, + "MCP handshake failed for MCPServer %s: %s", mcpServer.Name, message) } func (r *MCPServerReconciler) emitMCPHandshakeRetriesExhausted(mcpServer *mcpv1alpha1.MCPServer, retryCount int32) { diff --git a/internal/controller/mcpserver_controller_handshake_test.go b/internal/controller/mcpserver_controller_handshake_test.go index 29094e21..8f1a930b 100644 --- a/internal/controller/mcpserver_controller_handshake_test.go +++ b/internal/controller/mcpserver_controller_handshake_test.go @@ -487,6 +487,7 @@ var _ = Describe("MCPServer Controller - MCP Handshake Validation", func() { Eventually(fr.Events).Should(Receive(&handshakeFailedEvent)) Expect(handshakeFailedEvent).To(ContainSubstring(corev1.EventTypeWarning)) Expect(handshakeFailedEvent).To(ContainSubstring(ReasonMCPEndpointUnavailable)) + Expect(handshakeFailedEvent).To(ContainSubstring(resourceName)) Expect(handshakeFailedEvent).To(ContainSubstring(failMsg)) By("Second reconcile with same error — no duplicate handshake failed event") diff --git a/internal/controller/mcpserver_controller_test.go b/internal/controller/mcpserver_controller_test.go index cc2dbd44..145b7374 100644 --- a/internal/controller/mcpserver_controller_test.go +++ b/internal/controller/mcpserver_controller_test.go @@ -152,6 +152,7 @@ var _ = Describe("MCPServer Controller", func() { Eventually(fr.Events).Should(Receive(&first)) Expect(first).To(ContainSubstring(corev1.EventTypeNormal)) Expect(first).To(ContainSubstring(ReasonValid)) + Expect(first).To(ContainSubstring(resourceName)) Expect(first).To(ContainSubstring("Accepted=True")) _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ From b6f3b1f2285183a783787b96afef49b16a10a23e Mon Sep 17 00:00:00 2001 From: Adarsh Kumar Yadav Date: Thu, 21 May 2026 12:38:13 +0530 Subject: [PATCH 4/6] test(controller): assert second handshake-failed event on message change Extend the MCPHandshakeFailed dedupe test to cover both no-duplicate when the error is unchanged and a new event when the message changes, matching the test name and Copilot review feedback on #186. --- .../mcpserver_controller_handshake_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/internal/controller/mcpserver_controller_handshake_test.go b/internal/controller/mcpserver_controller_handshake_test.go index 8f1a930b..7e5b0edb 100644 --- a/internal/controller/mcpserver_controller_handshake_test.go +++ b/internal/controller/mcpserver_controller_handshake_test.go @@ -496,6 +496,21 @@ var _ = Describe("MCPServer Controller - MCP Handshake Validation", func() { }) Expect(err).NotTo(HaveOccurred()) Consistently(fr.Events, 300*time.Millisecond, 20*time.Millisecond).ShouldNot(Receive()) + + By("Change error message — second Warning event emitted") + failMsg = "different failure message" + _, err = reconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + var secondHandshakeFailedEvent string + Eventually(fr.Events).Should(Receive(&secondHandshakeFailedEvent)) + Expect(secondHandshakeFailedEvent).To(ContainSubstring(corev1.EventTypeWarning)) + Expect(secondHandshakeFailedEvent).To(ContainSubstring(ReasonMCPEndpointUnavailable)) + Expect(secondHandshakeFailedEvent).To(ContainSubstring(resourceName)) + Expect(secondHandshakeFailedEvent).To(ContainSubstring(failMsg)) + Expect(secondHandshakeFailedEvent).NotTo(Equal(handshakeFailedEvent)) }) It("should emit MCPHandshakeRetriesExhausted once when max handshake retries is reached", func() { From 099e114d30ba8a7de69f0a27163b1a892ca39be6 Mon Sep 17 00:00:00 2001 From: Adarsh Kumar Yadav Date: Thu, 21 May 2026 19:59:12 +0530 Subject: [PATCH 5/6] test(controller): replace goto with drainEvents in exhausted test Address aliok review on #186 by draining fake recorder events via a shared helper instead of goto in the retries-exhausted assertion. --- .../mcpserver_controller_handshake_test.go | 10 +--------- internal/controller/mcpserver_controller_test.go | 13 +++++++++++++ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/internal/controller/mcpserver_controller_handshake_test.go b/internal/controller/mcpserver_controller_handshake_test.go index 7e5b0edb..614d6388 100644 --- a/internal/controller/mcpserver_controller_handshake_test.go +++ b/internal/controller/mcpserver_controller_handshake_test.go @@ -552,15 +552,7 @@ var _ = Describe("MCPServer Controller - MCP Handshake Validation", func() { var collected []string Eventually(func(g Gomega) { - for { - select { - case ev := <-fr.Events: - collected = append(collected, ev) - default: - goto check - } - } - check: + collected = drainEvents(fr.Events) exhausted := 0 for _, ev := range collected { if strings.Contains(ev, "retries exhausted") { diff --git a/internal/controller/mcpserver_controller_test.go b/internal/controller/mcpserver_controller_test.go index 145b7374..54d4c30c 100644 --- a/internal/controller/mcpserver_controller_test.go +++ b/internal/controller/mcpserver_controller_test.go @@ -74,6 +74,19 @@ func drainFakeRecorderEvents(fr *events.FakeRecorder) { } } +// drainEvents returns all currently buffered events from ch without blocking. +func drainEvents(ch <-chan string) []string { + var events []string + for { + select { + case ev := <-ch: + events = append(events, ev) + default: + return events + } + } +} + // newTestMCPServer returns an MCPServer with standard test defaults: // namespace "default", SourceTypeContainerImage with ref // "docker.io/library/test-image:latest", and port 8080. From 627be9c3e86dd1f3ed0e47043abccf32aaa8e485 Mon Sep 17 00:00:00 2001 From: Adarsh Kumar Yadav Date: Thu, 21 May 2026 20:02:23 +0530 Subject: [PATCH 6/6] test(controller): fix revive import-shadowing in drainEvents Rename local slice to avoid shadowing k8s.io/client-go/tools/events. --- internal/controller/mcpserver_controller_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/controller/mcpserver_controller_test.go b/internal/controller/mcpserver_controller_test.go index 54d4c30c..ed8f06f5 100644 --- a/internal/controller/mcpserver_controller_test.go +++ b/internal/controller/mcpserver_controller_test.go @@ -76,13 +76,13 @@ func drainFakeRecorderEvents(fr *events.FakeRecorder) { // drainEvents returns all currently buffered events from ch without blocking. func drainEvents(ch <-chan string) []string { - var events []string + var drained []string for { select { case ev := <-ch: - events = append(events, ev) + drained = append(drained, ev) default: - return events + return drained } } }