@@ -248,6 +248,101 @@ case "${mode}" in
248248 autoscaler-absent)
249249 SECTION="cluster-autoscaling"
250250 ;;
251+ dynamo-dispatch-list-failed)
252+ # Drives the REAL collect_service_metrics dispatcher (no override): the
253+ # Dynamo pod list fails while the namespace exists. The dispatcher must
254+ # route to the Dynamo collector so the failure surfaces, rather than
255+ # falling through to NIM/trainer and emitting unrelated evidence.
256+ kubectl() {
257+ if [ "${1:-}" = "cluster-info" ]; then
258+ return 0
259+ fi
260+ case " $* " in
261+ *"dynamo-component-type=worker"*) return 1 ;;
262+ *" get namespace dynamo-workload "*) return 0 ;;
263+ esac
264+ return 0
265+ }
266+ SECTION="service-metrics"
267+ ;;
268+ dynamo-dispatch-both-queries-failed)
269+ # Both Dynamo probes fail (cluster-wide read failure: RBAC revoked,
270+ # expired credentials, apiserver outage). The namespace state cannot be
271+ # classified, so the dispatcher must NOT read that as "no workload" and
272+ # fall through to NIM/trainer — it routes to Dynamo, which fails closed.
273+ kubectl() {
274+ if [ "${1:-}" = "cluster-info" ]; then
275+ return 0
276+ fi
277+ case " $* " in
278+ *"dynamo-component-type=worker"*) return 1 ;;
279+ *" get namespace dynamo-workload "*)
280+ echo "Error from server (Forbidden): namespaces \"dynamo-workload\" is forbidden" >&2
281+ return 1 ;;
282+ esac
283+ return 0
284+ }
285+ SECTION="service-metrics"
286+ ;;
287+ dynamo-workload-absent)
288+ # No worker pods: the section MEASURES an existing workload and must
289+ # never deploy one, so an absent workload is an absent prerequisite
290+ # (SKIP), not a cue to apply the manifest.
291+ kubectl() {
292+ if [ "${1:-}" = "cluster-info" ]; then
293+ return 0
294+ fi
295+ return 0
296+ }
297+ collect_service_metrics() {
298+ EVIDENCE_FILE="${EVIDENCE_DIR}/ai-service-metrics.md"
299+ collect_service_metrics_dynamo
300+ }
301+ SECTION="service-metrics"
302+ ;;
303+ dynamo-workload-list-failed)
304+ # The pod list fails: a read error must fail closed rather than be
305+ # flattened into "no workload present".
306+ kubectl() {
307+ if [ "${1:-}" = "cluster-info" ]; then
308+ return 0
309+ fi
310+ case " $* " in
311+ *"dynamo-component-type=worker"*) return 1 ;;
312+ esac
313+ return 0
314+ }
315+ collect_service_metrics() {
316+ EVIDENCE_FILE="${EVIDENCE_DIR}/ai-service-metrics.md"
317+ collect_service_metrics_dynamo
318+ }
319+ SECTION="service-metrics"
320+ ;;
321+ hpa-scaled-cleanup-ok|hpa-scaled-cleanup-failed)
322+ # Both lanes observe a real scale-up (replicas>1 with a current metric),
323+ # so hpa_scaled is true and the ONLY difference is whether the test
324+ # namespace could be deleted. The section must not report PASS while its
325+ # unbounded CUDA workload may still be running, so the cleanup lane has
326+ # to flip the verdict to FAIL — and the control lane proves the FAIL is
327+ # attributable to cleanup rather than to a stub that never passes.
328+ kubectl() {
329+ if [ "${1:-}" = "cluster-info" ]; then
330+ return 0
331+ fi
332+ case " $* " in
333+ *"averageValue"*) echo "75" ;;
334+ *"currentReplicas"*) echo "2" ;;
335+ esac
336+ return 0
337+ }
338+ sleep() { return 0; }
339+ if [ "${mode}" = "hpa-scaled-cleanup-failed" ]; then
340+ cleanup_ns() { return 1; }
341+ else
342+ cleanup_ns() { return 0; }
343+ fi
344+ SECTION="hpa"
345+ ;;
251346 dynamo-unhealthy)
252347 kubectl() {
253348 if [ "${1:-}" = "cluster-info" ]; then
@@ -526,6 +621,58 @@ func TestEvidenceResultPropagatesThroughCollector(t *testing.T) {
526621 wantStatus : "FAIL" ,
527622 wantErr : true ,
528623 },
624+ {
625+ name : "dispatcher routes a failed Dynamo list to the Dynamo collector" ,
626+ mode : "dynamo-dispatch-list-failed" ,
627+ displayName : "AI Service Metrics" ,
628+ wantStatus : "FAIL" ,
629+ wantEvidence : "the workload state is unknown" ,
630+ singleVerdictFile : "ai-service-metrics.md" ,
631+ wantErr : true ,
632+ },
633+ {
634+ name : "unclassifiable Dynamo namespace state fails closed" ,
635+ mode : "dynamo-dispatch-both-queries-failed" ,
636+ displayName : "AI Service Metrics" ,
637+ wantStatus : "FAIL" ,
638+ wantEvidence : "the workload state is unknown" ,
639+ singleVerdictFile : "ai-service-metrics.md" ,
640+ wantErr : true ,
641+ },
642+ {
643+ name : "absent Dynamo workload skips instead of deploying" ,
644+ mode : "dynamo-workload-absent" ,
645+ displayName : "AI Service Metrics" ,
646+ wantStatus : "SKIP" ,
647+ wantEvidence : "no running Dynamo workload in dynamo-workload" ,
648+ singleVerdictFile : "ai-service-metrics.md" ,
649+ },
650+ {
651+ name : "Dynamo worker pod list failure fails closed" ,
652+ mode : "dynamo-workload-list-failed" ,
653+ displayName : "AI Service Metrics" ,
654+ wantStatus : "FAIL" ,
655+ wantEvidence : "the workload state is unknown" ,
656+ singleVerdictFile : "ai-service-metrics.md" ,
657+ wantErr : true ,
658+ },
659+ {
660+ name : "HPA scale-up with successful cleanup is pass" ,
661+ mode : "hpa-scaled-cleanup-ok" ,
662+ displayName : "Pod Autoscaling (HPA)" ,
663+ wantStatus : "PASS" ,
664+ singleVerdictFile : "pod-autoscaling.md" ,
665+ wantErr : false ,
666+ },
667+ {
668+ name : "HPA scale-up with failed cleanup is fail" ,
669+ mode : "hpa-scaled-cleanup-failed" ,
670+ displayName : "Pod Autoscaling (HPA)" ,
671+ wantStatus : "FAIL" ,
672+ wantEvidence : "the hpa-test namespace could not be deleted" ,
673+ singleVerdictFile : "pod-autoscaling.md" ,
674+ wantErr : true ,
675+ },
529676 {
530677 name : "present but unhealthy Dynamo is fail" ,
531678 mode : "dynamo-unhealthy" ,
@@ -598,6 +745,16 @@ func TestEvidenceResultPropagatesThroughCollector(t *testing.T) {
598745 t .Fatalf ("write trainer manifest fixture: %v" , err )
599746 }
600747 }
748+ if strings .HasPrefix (tt .mode , "hpa-" ) {
749+ manifestDir := filepath .Join (dir , "manifests" )
750+ if err := os .MkdirAll (manifestDir , 0o755 ); err != nil {
751+ t .Fatalf ("create manifest directory: %v" , err )
752+ }
753+ manifestPath := filepath .Join (manifestDir , "hpa-gpu-test.yaml" )
754+ if err := os .WriteFile (manifestPath , []byte ("---\n " ), 0o600 ); err != nil {
755+ t .Fatalf ("write HPA manifest fixture: %v" , err )
756+ }
757+ }
601758 if strings .HasPrefix (tt .mode , "gang-" ) {
602759 manifestDir := filepath .Join (dir , "manifests" )
603760 if err := os .MkdirAll (manifestDir , 0o755 ); err != nil {
@@ -652,7 +809,13 @@ func TestEvidenceResultPropagatesThroughCollector(t *testing.T) {
652809 t .Errorf ("result = %q, want %q" , got , want )
653810 }
654811 if tt .wantEvidence != "" {
655- evidencePath := filepath .Join (outputDir , "ai-service-metrics.md" )
812+ // Assert against the artifact the case names, falling back to
813+ // the service-metrics file the original lanes all used.
814+ evidenceName := tt .singleVerdictFile
815+ if evidenceName == "" {
816+ evidenceName = "ai-service-metrics.md"
817+ }
818+ evidencePath := filepath .Join (outputDir , evidenceName )
656819 evidence , evidenceErr := os .ReadFile (evidencePath )
657820 if evidenceErr != nil {
658821 t .Fatalf ("read evidence artifact: %v" , evidenceErr )
0 commit comments