Skip to content

test(reconcile): add unit tests for componentReconciler#3003

Open
github-actions[bot] wants to merge 1 commit intomainfrom
daily-builder/reconcile-tests-09e04e7fee5dea65
Open

test(reconcile): add unit tests for componentReconciler#3003
github-actions[bot] wants to merge 1 commit intomainfrom
daily-builder/reconcile-tests-09e04e7fee5dea65

Conversation

@github-actions
Copy link
Contributor

Source

Backlog improvement — coverage gap identified during research phase (discussion #2812). pkg/cli/cmd/cluster/reconcile.go had zero test coverage despite containing critical component reconciliation logic.

Goal and Rationale

Add unit tests for the componentReconciler in pkg/cli/cmd/cluster/reconcile.go. This struct handles in-place component updates (CNI, CSI, MetricsServer, LoadBalancer, CertManager, PolicyEngine, GitOpsEngine) during ksail cluster update. Testing it closes a significant coverage gap and protects against regressions in the update path.

Approach

New test file

pkg/cli/cmd/cluster/reconcile_test.go — 14 tests covering:

  • TestHandlerForField_KnownFields — table-driven, all 7 registered component fields return a handler
  • TestHandlerForField_UnknownField — unrecognised fields (nodes, mirrorRegistries, empty) return false
  • TestReconcileMetricsServer_DisabledReturnsError — disabling metrics-server in-place returns errMetricsServerDisableUnsupported
  • TestReconcileCSI_NilFactory — nil CSI factory returns ErrCSIInstallerFactoryNil
  • TestReconcileCSI_DisabledToDisabled_Noop — documents that nil-factory check fires before the disabled no-op guard
  • TestReconcileCertManager_DisabledFromDisabled_Noop — disabled→disabled is a no-op (factory is non-nil but never called)
  • TestReconcileCertManager_DisabledFromEnabled_NilFactory — enabled→disabled with nil factory returns ErrCertManagerInstallerFactoryNil
  • TestReconcilePolicyEngine_NoneToNone_Noop — None→None is a no-op even with nil factory (nil check is guarded by old-value branch)
  • TestReconcilePolicyEngine_NoneFromEnabled_NilFactory — Kyverno→None with nil factory returns ErrPolicyEngineInstallerFactoryNil
  • TestReconcileGitOpsEngine_NoneToNone_Noop — no-op, no factory needed
  • TestReconcileGitOpsEngine_EmptyToEmpty_Noop — empty→empty also no-op
  • TestReconcileComponents_EmptyDiff — empty diff produces no applied/failed changes
  • TestReconcileComponents_UnknownField_Skipped — unregistered fields are silently skipped
  • TestReconcileComponents_RecordsFailedChange — component error is captured in result.FailedChanges
  • TestReconcileComponents_MixedKnownAndUnknown — mixed diff: unknown skipped, disabled no-op counted as applied

Supporting additions

  • pkg/cli/cmd/cluster/testing.go: SetPolicyEngineInstallerFactoryForTests DI helper (same pattern as existing CSI/CertManager/ArgoCD helpers)
  • pkg/cli/cmd/cluster/export_test.go: ExportErrMetricsServerDisableUnsupported, ExportHandlerForField, ExportReconcileMetricsServer, ExportReconcileCSI, ExportReconcileCertManager, ExportReconcilePolicyEngine, ExportReconcileGitOpsEngine, ExportReconcileComponents; added context import

Impact

  • reconcile.go goes from 0% to meaningful unit test coverage
  • All testable paths (nil guards, disabled no-ops, unknown-field skipping, failure recording) are exercised without network or Docker dependency
  • New DI helper completes the factory-override set (CSI, CertManager, ArgoCD, PolicyEngine now all covered)

Validation

Tests are pure unit tests with no external dependencies (no Docker, no Kubernetes API). CI will run them with Go 1.26.0 on push.

Future Work

  • reconcileCNI, reconcileLoadBalancer, and the install paths of CSI/CertManager/PolicyEngine/GitOpsEngine could be covered with additional mock installers
  • The uninstallWithFactory helper path could be tested with a mock installer.Installer

Generated by Daily Builder ·

Warning

⚠️ Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • proxy.golang.org

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "proxy.golang.org"

See Network Configuration for more information.

Cover handlerForField field registration, reconcileMetricsServer
disabled error, reconcileCSI/CertManager/PolicyEngine nil-factory
errors, reconcileGitOpsEngine None→None no-op, and reconcileComponents
empty-diff, unknown-field, and failure-recording paths.

Also adds SetPolicyEngineInstallerFactoryForTests DI helper and
exports (ExportHandlerForField, ExportReconcile*, etc.) in
export_test.go so the unexported reconciler methods are reachable
from the external test package.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@devantler devantler marked this pull request as ready for review March 12, 2026 11:58
@devantler devantler self-requested a review as a code owner March 12, 2026 11:58
Copilot AI review requested due to automatic review settings March 12, 2026 11:58
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds unit tests around componentReconciler in the cluster update path to close a major coverage gap in pkg/cli/cmd/cluster/reconcile.go, using existing test-time DI hooks for installer factories and new test-only exports.

Changes:

  • Added pkg/cli/cmd/cluster/reconcile_test.go with unit tests covering handler dispatch, no-op/unsupported transitions, nil-factory guards, and reconcile result recording.
  • Added a DI helper to override the PolicyEngine installer factory for tests.
  • Extended export_test.go with test-only exports to exercise unexported reconciler logic and sentinel errors.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
pkg/cli/cmd/cluster/testing.go Adds SetPolicyEngineInstallerFactoryForTests to complete installer-factory DI overrides for unit tests.
pkg/cli/cmd/cluster/reconcile_test.go Introduces new unit tests for component reconciliation behavior and result recording.
pkg/cli/cmd/cluster/export_test.go Adds test-only exports to reach unexported reconciler functions and sentinel error values.

Comment on lines +120 to +142
// TestReconcileCSI_DisabledToDisabled_Noop verifies that transitioning from disabled to
// disabled is a no-op regardless of the factory state.
func TestReconcileCSI_DisabledToDisabled_Noop(t *testing.T) {
t.Parallel()

// nil CSI factory — but the no-op path returns before the nil check
restore := clusterpkg.SetCSIInstallerFactoryForTests(nil)
t.Cleanup(restore)

cmd := newReconcileTestCmd()
clusterCfg := newReconcileTestClusterCfg()
change := clusterupdate.Change{
Field: "cluster.csi",
OldValue: string(v1alpha1.CSIDisabled),
NewValue: string(v1alpha1.CSIDisabled),
}

// The nil-check guard fires before the no-op check, so this returns an error.
// Document this known behaviour to prevent regressions.
err := clusterpkg.ExportReconcileCSI(cmd, clusterCfg, change)
require.Error(t, err, "nil factory is checked before the disabled no-op path")
assert.ErrorIs(t, err, setup.ErrCSIInstallerFactoryNil)
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test name/comment says disabled→disabled is a no-op regardless of factory state, but the assertions expect an error because the nil-factory guard runs first. Please update the test name/comment to match the actual behavior being validated (or adjust the production code if the intended behavior is truly a no-op).

Copilot uses AI. Check for mistakes.
Comment on lines +312 to +331
// TestReconcileComponents_RecordsFailedChange verifies that a component error is captured
// in result.FailedChanges and the reconciler continues processing remaining changes.
func TestReconcileComponents_RecordsFailedChange(t *testing.T) {
t.Parallel()

// Null CSI factory so the reconcileCSI call will fail immediately.
restore := clusterpkg.SetCSIInstallerFactoryForTests(nil)
t.Cleanup(restore)

cmd := newReconcileTestCmd()
clusterCfg := newReconcileTestClusterCfg()
diff := &clusterupdate.UpdateResult{
InPlaceChanges: []clusterupdate.Change{
{
Field: "cluster.csi",
OldValue: string(v1alpha1.CSIDisabled),
NewValue: "hetznercsi",
},
},
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test’s doc comment says the reconciler “continues processing remaining changes”, but the diff only contains a single change, so the behavior isn’t actually exercised. Either add an additional in-place change after the failing one and assert it’s still applied/skipped as expected, or remove the claim from the comment to avoid misleading future readers.

Copilot uses AI. Check for mistakes.
Comment on lines +136 to +142
// ExportHandlerForField reports whether a registered handler exists for the given field name.
var ExportHandlerForField = func(cmd *cobra.Command, clusterCfg *v1alpha1.Cluster, field string) bool {
r := newComponentReconciler(cmd, clusterCfg)
_, ok := r.handlerForField(field)

return ok
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this file the existing exports are plain functions (e.g., ExportShouldPushOCIArtifact), but these new exports are declared as assignable variables holding function literals. Since they aren’t meant to be overridden, defining them as regular functions would be more consistent and avoids accidental reassignment from other tests.

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +106
func TestReconcileCSI_NilFactory(t *testing.T) {
t.Parallel()

restore := clusterpkg.SetCSIInstallerFactoryForTests(nil)
t.Cleanup(restore)

cmd := newReconcileTestCmd()
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests call Set*InstallerFactoryForTests (which mutates the package-level installerFactoriesOverride) but also run with t.Parallel(). Because the override is global to the cluster package, parallel execution can interleave overrides/restores across tests and make outcomes flaky/non-deterministic. Consider removing t.Parallel() from any test that overrides factories, or serialize overrides with a package-level mutex in the test file (so only one override-based test runs at a time).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: 🫴 Ready

Development

Successfully merging this pull request may close these issues.

1 participant