@@ -565,3 +565,88 @@ func TestWaitForDeclaredTrainer_LateSuccessDoesNotOutrunTheDeadline(t *testing.T
565565 t .Errorf ("reason claims nothing was found, but the probe returned complete: %v" , err )
566566 }
567567}
568+
569+ // TestWaitForDeclaredTrainer_ExpiryNamesTheProbeThatJustRan pins the reason reported
570+ // when the allowance expires on an *incomplete* probe.
571+ //
572+ // The recheck before accepting a result runs on every iteration, not only the
573+ // successful ones. When the probe that just ran came back incomplete, its own
574+ // finding — the object that is actually missing — is what the operator needs. An
575+ // earlier revision carried the previous iteration's result into the diagnosis, so
576+ // the first probe of a wait reported the zero value and the operator got a generic
577+ // "no complete installation was found" instead of the missing CRD.
578+ //
579+ // Stalling the CRD read past the allowance and answering NotFound produces exactly
580+ // that shape: one probe, incomplete, deadline already gone.
581+ func TestWaitForDeclaredTrainer_ExpiryNamesTheProbeThatJustRan (t * testing.T ) {
582+ oldTimeout , oldInterval := trainerInstallWaitTimeout , trainerInstallPollInterval
583+ trainerInstallWaitTimeout = 20 * time .Millisecond
584+ trainerInstallPollInterval = time .Millisecond
585+ defer func () {
586+ trainerInstallWaitTimeout , trainerInstallPollInterval = oldTimeout , oldInterval
587+ }()
588+
589+ client := newTrainerFakeClient ()
590+ client .PrependReactor ("get" , "customresourcedefinitions" ,
591+ func (k8stesting.Action ) (bool , runtime.Object , error ) {
592+ // Outlive the allowance, then answer NotFound. A missing CRD short-circuits
593+ // the probe, so it returns incomplete with no error and the recheck below
594+ // is the next thing that runs.
595+ time .Sleep (60 * time .Millisecond )
596+ return true , nil , apierrors .NewNotFound (
597+ schema.GroupResource {Group : "apiextensions.k8s.io" , Resource : "customresourcedefinitions" },
598+ "trainjobs.trainer.kubeflow.org" )
599+ })
600+
601+ _ , err := waitForDeclaredTrainer (context .Background (), client )
602+ if err == nil {
603+ t .Fatal ("expected an error once the allowance expired with the installation incomplete" )
604+ }
605+ if ! stderrors .Is (err , aicrErrors .New (aicrErrors .ErrCodeNotFound , "" )) {
606+ t .Errorf ("error code = %v, want ErrCodeNotFound: the deadline expired locally " +
607+ "with the parent still live" , err )
608+ }
609+ if ! strings .Contains (err .Error (), "CRD trainjobs.trainer.kubeflow.org is missing" ) {
610+ t .Errorf ("expiry did not name the object the probe just found missing: %v" , err )
611+ }
612+ if strings .Contains (err .Error (), "no complete installation was found" ) {
613+ t .Errorf ("expiry fell back to the generic reason even though the probe that just " +
614+ "ran reported a specific missing object: %v" , err )
615+ }
616+ }
617+
618+ // TestWaitForDeclaredTrainer_LateSuccessClaimsOnlyWhatWasObserved pins the wording of
619+ // the late-success reason to what the code can actually know.
620+ //
621+ // The recheck learns that the probe returned complete and that the deadline has
622+ // already passed. It cannot know *when* the installation became complete — only when
623+ // it was observed to be. Claiming it "became complete after the allowance expired"
624+ // asserts a transition time nobody measured, and would mislead an operator whose
625+ // Trainer was healthy all along behind a slow apiserver.
626+ func TestWaitForDeclaredTrainer_LateSuccessClaimsOnlyWhatWasObserved (t * testing.T ) {
627+ oldTimeout , oldInterval := trainerInstallWaitTimeout , trainerInstallPollInterval
628+ trainerInstallWaitTimeout = 20 * time .Millisecond
629+ trainerInstallPollInterval = time .Millisecond
630+ defer func () {
631+ trainerInstallWaitTimeout , trainerInstallPollInterval = oldTimeout , oldInterval
632+ }()
633+
634+ client := newTrainerFakeClient (completeTrainerInstall ()... )
635+ client .PrependReactor ("get" , "services" ,
636+ func (k8stesting.Action ) (bool , runtime.Object , error ) {
637+ time .Sleep (60 * time .Millisecond )
638+ return false , nil , nil
639+ })
640+
641+ _ , err := waitForDeclaredTrainer (context .Background (), client )
642+ if err == nil {
643+ t .Fatal ("expected an error once the allowance expired" )
644+ }
645+ if strings .Contains (err .Error (), "became complete" ) {
646+ t .Errorf ("reason asserts when the installation became complete, which the wait " +
647+ "never measured; it only observed completeness after expiry: %v" , err )
648+ }
649+ if ! strings .Contains (err .Error (), "observed complete only after the allowance expired" ) {
650+ t .Errorf ("reason does not state what was actually observed: %v" , err )
651+ }
652+ }
0 commit comments