From 917497152be734bd5ce6c0069795ee4373c22166 Mon Sep 17 00:00:00 2001 From: Christopher Maher Date: Thu, 9 Jul 2026 23:17:36 -0700 Subject: [PATCH] test(foreman): lock in sliced-workload verdict rollup (Task 5) Task 5 of the Sliced Workloads epic (#1033): confirm a sliced Workload's integrate/reconcile steps roll up to the Workload correctly. No production wiring is needed: classifyChildren is verdict-based, not kind-based, so a Succeeded + GATE-FAIL reconcile (pinned interface drift) or integrate (overlap / stale-base apply) already lands in the incomplete bucket and keeps the Workload out of Completed, while a clean GATE-PASS counts as succeeded. Adds a classifyChildren unit test over the integrate/reconcile kinds x GATE-PASS/GATE-FAIL so a future kind-special-casing change can't silently regress the slicer's reliance on this, plus a doc note recording it. Part of #1033 Signed-off-by: Christopher Maher --- .../foreman/controller/workload_controller.go | 6 ++ .../controller/workload_rollup_slicer_test.go | 60 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 internal/foreman/controller/workload_rollup_slicer_test.go diff --git a/internal/foreman/controller/workload_controller.go b/internal/foreman/controller/workload_controller.go index 7e23df00..c408716c 100644 --- a/internal/foreman/controller/workload_controller.go +++ b/internal/foreman/controller/workload_controller.go @@ -567,6 +567,12 @@ type childCounts struct { // - failed: Phase=Failed // - inFlight: everything else (Pending / Scheduled / Running) // +// Classification is verdict-based, not kind-based: a sliced Workload's +// integrate / reconcile steps (#1033) land here like any other kind, so a +// GATE-FAIL reconcile (pinned interface drift) or integrate (overlap / +// stale-base apply) counts as incomplete and keeps the Workload out of +// Completed with no slicer-specific rollup wiring. +// // Skipped matches BEFORE the generic "Phase=Succeeded not on-target" // case so its body (empty) is selected; otherwise the generic case // would catch it. The empty body is intentional — Skipped children are diff --git a/internal/foreman/controller/workload_rollup_slicer_test.go b/internal/foreman/controller/workload_rollup_slicer_test.go new file mode 100644 index 00000000..19893bf9 --- /dev/null +++ b/internal/foreman/controller/workload_rollup_slicer_test.go @@ -0,0 +1,60 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "testing" + + foremanv1alpha1 "github.com/defilantech/llmkube/api/foreman/v1alpha1" +) + +// TestClassifyChildren_SlicerVerdicts locks in that a sliced Workload's +// integrate and reconcile steps roll up through the same verdict-based +// classification as every other kind (#1033). The rollup is deliberately +// kind-agnostic: a Succeeded + GATE-FAIL reconcile (pinned interface drift) or +// integrate (overlap / stale-base apply) lands in the incomplete bucket, which +// keeps the Workload out of Completed; a clean GATE-PASS counts as succeeded. +// If a future change special-cases kinds in classifyChildren, this fails. +func TestClassifyChildren_SlicerVerdicts(t *testing.T) { + mk := func(kind foremanv1alpha1.AgenticTaskKind, verdict foremanv1alpha1.AgenticTaskVerdict) foremanv1alpha1.AgenticTask { + return foremanv1alpha1.AgenticTask{ + Spec: foremanv1alpha1.AgenticTaskSpec{Kind: kind}, + Status: foremanv1alpha1.AgenticTaskStatus{Phase: foremanv1alpha1.AgenticTaskPhaseSucceeded, Verdict: verdict}, + } + } + + tests := []struct { + name string + task foremanv1alpha1.AgenticTask + wantSucceeded int32 + wantIncomplete int32 + }{ + {"integrate clean", mk(foremanv1alpha1.AgenticTaskKindIntegrate, foremanv1alpha1.AgenticTaskVerdictGatePass), 1, 0}, + {"integrate overlap", mk(foremanv1alpha1.AgenticTaskKindIntegrate, foremanv1alpha1.AgenticTaskVerdictGateFail), 0, 1}, + {"reconcile clean", mk(foremanv1alpha1.AgenticTaskKindReconcile, foremanv1alpha1.AgenticTaskVerdictGatePass), 1, 0}, + {"reconcile drift", mk(foremanv1alpha1.AgenticTaskKindReconcile, foremanv1alpha1.AgenticTaskVerdictGateFail), 0, 1}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + c := classifyChildren([]foremanv1alpha1.AgenticTask{tc.task}) + if c.succeeded != tc.wantSucceeded || c.incomplete != tc.wantIncomplete { + t.Fatalf("classifyChildren = {succeeded:%d incomplete:%d}, want {succeeded:%d incomplete:%d}", + c.succeeded, c.incomplete, tc.wantSucceeded, tc.wantIncomplete) + } + }) + } +}