@@ -564,6 +564,217 @@ func TestReferencedDataStaleConditionGuard(t *testing.T) {
564564 }
565565}
566566
567+ // ─── Federation annotation bridge: terminal error propagation hub→cell ────────
568+
569+ // makeWDWithTerminalError returns a WD carrying the ReferencedDataErrorAnnotation
570+ // as a cell WD copy would after Karmada propagation from the hub.
571+ func makeWDWithTerminalError (reason , message string ) * computev1alpha.WorkloadDeployment {
572+ raw , _ := encodeTerminalError (reason , message )
573+ wd := & computev1alpha.WorkloadDeployment {
574+ ObjectMeta : metav1.ObjectMeta {
575+ Name : refDataTestDeployment ,
576+ Namespace : refDataTestNamespace ,
577+ UID : refDataTestWDUID ,
578+ Annotations : map [string ]string {
579+ computev1alpha .ReferencedDataErrorAnnotation : raw ,
580+ },
581+ },
582+ }
583+ return wd
584+ }
585+
586+ // TestFederated_TerminalAnnotation_SourceNotFound verifies that when the cell WD
587+ // copy carries the ReferencedDataErrorAnnotation with SourceNotFound, the Instance
588+ // gets Ready=False/SourceNotFound with the hub message rather than AwaitingPropagation.
589+ func TestFederated_TerminalAnnotation_SourceNotFound (t * testing.T ) {
590+ msg := `ConfigMap "app-config" not found in namespace "default"`
591+ wd := makeWDWithTerminalError (computev1alpha .ReferencedDataReasonSourceNotFound , msg )
592+ inst := makeInstanceWithRefDataGate ()
593+
594+ r , projectClient , _ := newRefDataReconciler (t , []client.Object {inst , wd })
595+ reconcileRefData (t , r )
596+
597+ var updated computev1alpha.Instance
598+ require .NoError (t , projectClient .Get (context .Background (),
599+ types.NamespacedName {Namespace : refDataTestNamespace , Name : refDataTestInstance }, & updated ))
600+
601+ cond := apimeta .FindStatusCondition (updated .Status .Conditions , computev1alpha .ReferencedDataReady )
602+ require .NotNil (t , cond , "ReferencedDataReady condition should be set" )
603+ assert .Equal (t , metav1 .ConditionFalse , cond .Status )
604+ assert .Equal (t , computev1alpha .ReferencedDataReasonSourceNotFound , cond .Reason ,
605+ "should use the hub resolver reason, not generic AwaitingPropagation" )
606+ assert .Equal (t , msg , cond .Message ,
607+ "should carry the hub resolver message verbatim" )
608+
609+ // Gate must remain until the error is resolved (companion will never arrive).
610+ hasGate := false
611+ if updated .Spec .Controller != nil {
612+ for _ , g := range updated .Spec .Controller .SchedulingGates {
613+ if g .Name == instancecontrol .ReferencedDataSchedulingGate .String () {
614+ hasGate = true
615+ }
616+ }
617+ }
618+ assert .True (t , hasGate , "ReferencedData gate should remain when a terminal error is present" )
619+ }
620+
621+ // TestFederated_TerminalAnnotation_SourceTooLarge verifies SourceTooLarge propagates
622+ // via the annotation the same way as SourceNotFound.
623+ func TestFederated_TerminalAnnotation_SourceTooLarge (t * testing.T ) {
624+ msg := `ConfigMap "fat-config" in namespace "default" exceeds per-object size limit`
625+ wd := makeWDWithTerminalError (computev1alpha .ReferencedDataReasonSourceTooLarge , msg )
626+ inst := makeInstanceWithRefDataGate ()
627+
628+ r , projectClient , _ := newRefDataReconciler (t , []client.Object {inst , wd })
629+ reconcileRefData (t , r )
630+
631+ var updated computev1alpha.Instance
632+ require .NoError (t , projectClient .Get (context .Background (),
633+ types.NamespacedName {Namespace : refDataTestNamespace , Name : refDataTestInstance }, & updated ))
634+
635+ cond := apimeta .FindStatusCondition (updated .Status .Conditions , computev1alpha .ReferencedDataReady )
636+ require .NotNil (t , cond )
637+ assert .Equal (t , metav1 .ConditionFalse , cond .Status )
638+ assert .Equal (t , computev1alpha .ReferencedDataReasonSourceTooLarge , cond .Reason )
639+ assert .Equal (t , msg , cond .Message )
640+ }
641+
642+ // TestFederated_TerminalAnnotation_SourceUnauthorized verifies SourceUnauthorized propagates
643+ // via the annotation.
644+ func TestFederated_TerminalAnnotation_SourceUnauthorized (t * testing.T ) {
645+ msg := `not authorized to read ConfigMap "secret-cfg" in namespace "default"`
646+ wd := makeWDWithTerminalError (computev1alpha .ReferencedDataReasonSourceUnauthorized , msg )
647+ inst := makeInstanceWithRefDataGate ()
648+
649+ r , projectClient , _ := newRefDataReconciler (t , []client.Object {inst , wd })
650+ reconcileRefData (t , r )
651+
652+ var updated computev1alpha.Instance
653+ require .NoError (t , projectClient .Get (context .Background (),
654+ types.NamespacedName {Namespace : refDataTestNamespace , Name : refDataTestInstance }, & updated ))
655+
656+ cond := apimeta .FindStatusCondition (updated .Status .Conditions , computev1alpha .ReferencedDataReady )
657+ require .NotNil (t , cond )
658+ assert .Equal (t , metav1 .ConditionFalse , cond .Status )
659+ assert .Equal (t , computev1alpha .ReferencedDataReasonSourceUnauthorized , cond .Reason )
660+ assert .Equal (t , msg , cond .Message )
661+ }
662+
663+ // TestFederated_NoAnnotation_AnnotationAbsent verifies that when the WD has no
664+ // terminal-error annotation AND no expected-data annotation (resolver not done),
665+ // the Instance gets Resolving/Unknown — NOT SourceNotFound.
666+ func TestFederated_NoAnnotation_AnnotationAbsent (t * testing.T ) {
667+ // WD has neither annotation — Karmada propagated it before the resolver ran.
668+ wd := makeWDForCell ("" ) // no annotations
669+ inst := makeInstanceWithRefDataGate ()
670+
671+ r , projectClient , _ := newRefDataReconciler (t , []client.Object {inst , wd })
672+ reconcileRefData (t , r )
673+
674+ var updated computev1alpha.Instance
675+ require .NoError (t , projectClient .Get (context .Background (),
676+ types.NamespacedName {Namespace : refDataTestNamespace , Name : refDataTestInstance }, & updated ))
677+
678+ cond := apimeta .FindStatusCondition (updated .Status .Conditions , computev1alpha .ReferencedDataReady )
679+ require .NotNil (t , cond )
680+ assert .Equal (t , metav1 .ConditionUnknown , cond .Status ,
681+ "should be Unknown/Resolving when no annotation is present" )
682+ assert .Equal (t , computev1alpha .ReferencedDataReasonResolving , cond .Reason )
683+ }
684+
685+ // TestFederated_AnnotationPresent_CompanionsMissing verifies that when the WD has
686+ // the expected-data annotation (resolver succeeded, no terminal error) but companions
687+ // haven't arrived yet, the Instance gets AwaitingPropagation — not SourceNotFound.
688+ func TestFederated_AnnotationPresent_CompanionsMissing (t * testing.T ) {
689+ expected := []string {refDataTestCMToken }
690+ annoVal , _ := json .Marshal (expected )
691+ // WD has the expected annotation but NOT the terminal-error annotation.
692+ wd := makeWDForCell (string (annoVal ))
693+ inst := makeInstanceWithRefDataGate ()
694+
695+ // No companion ConfigMap present yet.
696+ r , projectClient , _ := newRefDataReconciler (t , []client.Object {inst , wd })
697+ reconcileRefData (t , r )
698+
699+ var updated computev1alpha.Instance
700+ require .NoError (t , projectClient .Get (context .Background (),
701+ types.NamespacedName {Namespace : refDataTestNamespace , Name : refDataTestInstance }, & updated ))
702+
703+ cond := apimeta .FindStatusCondition (updated .Status .Conditions , computev1alpha .ReferencedDataReady )
704+ require .NotNil (t , cond )
705+ assert .Equal (t , metav1 .ConditionFalse , cond .Status )
706+ assert .Equal (t , computev1alpha .ReferencedDataReasonAwaitingPropagation , cond .Reason ,
707+ "should be AwaitingPropagation (not SourceNotFound) when resolver succeeded but companion is still in flight" )
708+ assert .Contains (t , cond .Message , refDataTestCMToken )
709+ }
710+
711+ // TestFederated_AnnotationPresent_CompanionsPresent verifies that when the WD
712+ // has the expected-data annotation and all companions are present (healthy path),
713+ // the Instance advances to Ready=True.
714+ func TestFederated_AnnotationPresent_CompanionsPresent (t * testing.T ) {
715+ expected := []string {refDataTestCMToken }
716+ annoVal , _ := json .Marshal (expected )
717+ wd := makeWDForCell (string (annoVal ))
718+ inst := makeInstanceWithRefDataGate ()
719+ companionCM := makeCompanionConfigMap (refDataTestCMCompanionName )
720+
721+ r , projectClient , _ := newRefDataReconciler (t , []client.Object {inst , wd , companionCM })
722+ reconcileRefData (t , r )
723+
724+ var updated computev1alpha.Instance
725+ require .NoError (t , projectClient .Get (context .Background (),
726+ types.NamespacedName {Namespace : refDataTestNamespace , Name : refDataTestInstance }, & updated ))
727+
728+ cond := apimeta .FindStatusCondition (updated .Status .Conditions , computev1alpha .ReferencedDataReady )
729+ require .NotNil (t , cond )
730+ assert .Equal (t , metav1 .ConditionTrue , cond .Status ,
731+ "should be Ready=True when all companions are present and no terminal error" )
732+ }
733+
734+ // TestFederated_QuotaAndSourceNotFound_SourceNotFoundWins verifies that when
735+ // both the Quota gate and ReferencedData gate are present, and the cell WD
736+ // carries a terminal-error annotation, Instance.Ready picks SourceNotFound
737+ // (priority 5) over PendingQuota (priority 3).
738+ func TestFederated_QuotaAndSourceNotFound_SourceNotFoundWins (t * testing.T ) {
739+ msg := `ConfigMap "app-config" not found in namespace "default"`
740+ wd := makeWDWithTerminalError (computev1alpha .ReferencedDataReasonSourceNotFound , msg )
741+ inst := makeInstanceWithRefDataGate ()
742+
743+ // Add the Quota gate alongside ReferencedData.
744+ inst .Spec .Controller .SchedulingGates = append (inst .Spec .Controller .SchedulingGates ,
745+ computev1alpha.SchedulingGate {Name : instancecontrol .QuotaSchedulingGate .String ()},
746+ )
747+
748+ // Seed a QuotaGranted=False/QuotaExceeded condition on the instance.
749+ inst .Status .Conditions = []metav1.Condition {
750+ {
751+ Type : computev1alpha .InstanceQuotaGranted ,
752+ Status : metav1 .ConditionFalse ,
753+ Reason : computev1alpha .InstanceQuotaGrantedReasonQuotaExceeded ,
754+ Message : "Quota exceeded for project" ,
755+ ObservedGeneration : 1 ,
756+ LastTransitionTime : metav1 .Now (),
757+ },
758+ }
759+
760+ r , projectClient , _ := newRefDataReconciler (t , []client.Object {inst , wd })
761+ reconcileRefData (t , r )
762+
763+ var updated computev1alpha.Instance
764+ require .NoError (t , projectClient .Get (context .Background (),
765+ types.NamespacedName {Namespace : refDataTestNamespace , Name : refDataTestInstance }, & updated ))
766+
767+ cond := apimeta .FindStatusCondition (updated .Status .Conditions , computev1alpha .ReferencedDataReady )
768+ require .NotNil (t , cond )
769+ assert .Equal (t , computev1alpha .ReferencedDataReasonSourceNotFound , cond .Reason ,
770+ "SourceNotFound should be set on ReferencedDataReady so reconcileGatedReadyCondition picks priority 5" )
771+
772+ // Note: reconcileGatedReadyCondition (called by reconcileInstanceReadyCondition,
773+ // not tested here) is where the priority competition between SourceNotFound (p5)
774+ // and PendingQuota (p3) is resolved. This test verifies that reconcileReferencedDataCondition
775+ // sets the right ReferencedDataReady sub-condition that feeds into that priority logic.
776+ }
777+
567778// containsAll returns true when s contains all substrings.
568779func containsAll (s string , subs ... string ) bool {
569780 for _ , sub := range subs {
0 commit comments