@@ -516,6 +516,126 @@ describe("run-crd carries every shared create-option or excludes it on purpose",
516516 } ) ;
517517} ) ;
518518
519+ describe ( "RunCrdWorkloadManager.create" , ( ) => {
520+ const manager = ( ) =>
521+ new RunCrdWorkloadManager ( {
522+ workloadApiProtocol : "http" ,
523+ workloadApiPort : 8020 ,
524+ namespace : "v4-runs" ,
525+ runtime : "microvm" ,
526+ } ) ;
527+
528+ beforeEach ( ( ) => {
529+ createRunner . mockReset ( ) ;
530+ createRunner . mockResolvedValue ( { metadata : { uid : "uid-created" } } ) ;
531+ getRunner . mockReset ( ) ;
532+ deleteRunner . mockReset ( ) ;
533+ deleteRunner . mockResolvedValue ( { } ) ;
534+ } ) ;
535+
536+ // createOptions() is dequeued at 03:00:00, so the default is an earlier delivery.
537+ const inTheWay = ( snapshotFriendlyID : string , dequeuedAt ?: string ) => ( {
538+ metadata : { name : "runner-abc123" , uid : "uid-existing" } ,
539+ spec : {
540+ bootstrap : {
541+ runFriendlyID : "run_abc123" ,
542+ snapshotFriendlyID,
543+ dequeuedAt : dequeuedAt ?? "2026-08-27T02:59:00.000Z" ,
544+ } ,
545+ } ,
546+ } ) ;
547+
548+ // The platform requeued an earlier delivery that never started the attempt.
549+ it ( "replaces a Runner an earlier delivery left, guarded by its uid" , async ( ) => {
550+ createRunner
551+ . mockRejectedValueOnce ( { code : 409 } )
552+ . mockResolvedValueOnce ( { metadata : { uid : "uid-recreated" } } ) ;
553+ getRunner . mockResolvedValue ( inTheWay ( "snapshot_earlier" ) ) ;
554+
555+ await manager ( ) . create ( createOptions ( ) ) ;
556+
557+ expect ( deleteRunner ) . toHaveBeenCalledWith (
558+ expect . objectContaining ( {
559+ name : "runner-abc123" ,
560+ body : { preconditions : { uid : "uid-existing" } } ,
561+ } )
562+ ) ;
563+ expect ( createRunner ) . toHaveBeenCalledTimes ( 2 ) ;
564+ } ) ;
565+
566+ // The first create found the old Runner's Secret, which the collector takes with it.
567+ it ( "gives the replacement a token Secret of its own" , async ( ) => {
568+ createRunner
569+ . mockRejectedValueOnce ( { code : 409 } )
570+ . mockResolvedValueOnce ( { metadata : { uid : "uid-recreated" } } ) ;
571+ getRunner . mockResolvedValue ( inTheWay ( "snapshot_earlier" ) ) ;
572+ createSecret . mockReset ( ) ;
573+ createSecret . mockResolvedValue ( { metadata : { uid : "uid-secret" } } ) ;
574+ patchObject . mockResolvedValue ( { } ) ;
575+
576+ await manager ( ) . create ( createOptions ( { deploymentToken : "tok" } ) ) ;
577+
578+ const [ first , second ] = createRunner . mock . calls . map (
579+ ( [ { body } ] ) => body . spec . deployment . token . name
580+ ) ;
581+ expect ( second ) . not . toBe ( first ) ;
582+ } ) ;
583+
584+ it ( "leaves a Runner this delivery already made" , async ( ) => {
585+ createRunner . mockRejectedValueOnce ( { code : 409 } ) ;
586+ getRunner . mockResolvedValue ( inTheWay ( "snapshot_abc" ) ) ;
587+
588+ await manager ( ) . create ( createOptions ( ) ) ;
589+
590+ expect ( deleteRunner ) . not . toHaveBeenCalled ( ) ;
591+ expect ( createRunner ) . toHaveBeenCalledTimes ( 1 ) ;
592+ } ) ;
593+
594+ // A delivery held up past the requeue must not remove the one that followed it.
595+ it . each ( [
596+ [ "a later delivery" , "2026-08-27T03:01:00.000Z" ] ,
597+ [ "a delivery dequeued at the same time" , "2026-08-27T03:00:00.000Z" ] ,
598+ [ "a delivery of unknown order" , "" ] ,
599+ ] ) ( "leaves a Runner from %s and fails as stale" , async ( _ , dequeuedAt ) => {
600+ createRunner . mockRejectedValueOnce ( { code : 409 } ) ;
601+ getRunner . mockResolvedValue ( inTheWay ( "snapshot_later" , dequeuedAt ) ) ;
602+
603+ await expect ( manager ( ) . create ( createOptions ( ) ) ) . rejects . toThrow ( "stale" ) ;
604+ expect ( deleteRunner ) . not . toHaveBeenCalled ( ) ;
605+ expect ( createRunner ) . toHaveBeenCalledTimes ( 1 ) ;
606+ } ) ;
607+
608+ // Another delivery of this snapshot made the replacement in the gap.
609+ it ( "accepts a replacement for this snapshot that another delivery made first" , async ( ) => {
610+ createRunner . mockRejectedValue ( { code : 409 } ) ;
611+ getRunner
612+ . mockResolvedValueOnce ( inTheWay ( "snapshot_earlier" ) )
613+ . mockResolvedValueOnce ( inTheWay ( "snapshot_abc" , "2026-08-27T03:00:00.000Z" ) ) ;
614+
615+ await manager ( ) . create ( createOptions ( ) ) ;
616+
617+ expect ( createRunner ) . toHaveBeenCalledTimes ( 2 ) ;
618+ } ) ;
619+
620+ it ( "replaces it only once per delivery" , async ( ) => {
621+ createRunner . mockRejectedValue ( { code : 409 } ) ;
622+ getRunner . mockResolvedValue ( inTheWay ( "snapshot_earlier" ) ) ;
623+
624+ await expect ( manager ( ) . create ( createOptions ( ) ) ) . rejects . toThrow ( "still in the way" ) ;
625+ expect ( createRunner ) . toHaveBeenCalledTimes ( 2 ) ;
626+ } ) ;
627+
628+ it ( "creates again when the Runner went between the create and the read" , async ( ) => {
629+ createRunner . mockRejectedValueOnce ( { code : 409 } ) ;
630+ getRunner . mockRejectedValue ( { code : 404 } ) ;
631+
632+ await manager ( ) . create ( createOptions ( ) ) ;
633+
634+ expect ( deleteRunner ) . not . toHaveBeenCalled ( ) ;
635+ expect ( createRunner ) . toHaveBeenCalledTimes ( 2 ) ;
636+ } ) ;
637+ } ) ;
638+
519639describe ( "RunCrdWorkloadManager.restore" , ( ) => {
520640 const checkpoint = { id : "checkpoint_abc" , location : "node-a/6f1c2a9e-snap" } ;
521641
@@ -709,12 +829,6 @@ describe("RunCrdWorkloadManager.restore", () => {
709829 expect ( createRunner ) . toHaveBeenCalledTimes ( 2 ) ;
710830 } ) ;
711831
712- it ( "still fails a cold start that finds a Runner in the way" , async ( ) => {
713- createRunner . mockRejectedValue ( { code : 409 } ) ;
714-
715- await expect ( manager ( ) . create ( createOptions ( ) ) ) . rejects . toEqual ( { code : 409 } ) ;
716- } ) ;
717-
718832 it ( "deletes a failed resume only while it is the Runner the watch saw" , async ( ) => {
719833 deleteRunner . mockReset ( ) ;
720834 deleteRunner . mockResolvedValue ( { } ) ;
0 commit comments