@@ -491,6 +491,52 @@ public function testAutoSoftDeletedDevVersionIsRecoveredWhenBranchReappearsWithN
491491 self ::assertSame ('newref9999999999 ' , $ reloaded ->getSource ()['reference ' ] ?? null );
492492 }
493493
494+ public function testDeleteBeforeWipesDevRowsButPreservesStableAndSoftDeletedStable (): void
495+ {
496+ // Seed three rows: an active stable, a maintainer-soft-deleted stable, and a dev branch.
497+ // DELETE_BEFORE must wipe only the dev row, leave both stable rows untouched, and the
498+ // post-loop prune must not crash on the survivors after $em->refresh($package).
499+ $ this ->seedStableVersion ($ this ->package , '1.0.0 ' , '1.0.0.0 ' , 'abcdef1234567890 ' );
500+ $ softDeleted = $ this ->seedStableVersion ($ this ->package , '1.1.0 ' , '1.1.0.0 ' , '1234567890abcdef ' );
501+ $ softDeleted ->setSoftDeletedAt (new \DateTimeImmutable ('-2 hours ' ));
502+ $ softDeleted ->setDeletionReason (VersionDeletionReason::DeletedByMaintainer);
503+ $ this ->seedDevVersion ($ this ->package , 'dev-main ' , 'dev-main ' , 'devref1234567890 ' );
504+ self ::getEM ()->persist ($ softDeleted );
505+ self ::getEM ()->flush ();
506+
507+ // Upstream only returns the active stable version — dev-main has disappeared from upstream
508+ // and 1.1.0 is also missing (consistent with the maintainer pull).
509+ $ upstream = $ this ->buildCompletePackage ('test/pkg ' , '1.0.0 ' , '1.0.0.0 ' , 'abcdef1234567890 ' );
510+ $ this ->repositoryMock = $ this ->createStub (VcsRepository::class);
511+ $ this ->repositoryMock ->method ('getPackages ' )->willReturn ([$ upstream ]);
512+ $ this ->repositoryMock ->method ('getDriver ' )->willReturn ($ this ->stableDriver ());
513+
514+ $ packageManagerMock = $ this ->createMock (PackageManager::class);
515+ $ packageManagerMock ->expects ($ this ->never ())->method ('notifyVersionReferenceChangeBlocked ' );
516+ $ this ->rebuildUpdater ($ packageManagerMock );
517+
518+ $ this ->updater ->update ($ this ->ioMock , $ this ->config , $ this ->package , $ this ->repositoryMock , Updater::DELETE_BEFORE );
519+
520+ $ em = self ::getEM ();
521+ $ em ->clear ();
522+ $ versionRepo = $ em ->getRepository (Version::class);
523+
524+ $ active = $ versionRepo ->findOneBy (['name ' => 'test/pkg ' , 'normalizedVersion ' => '1.0.0.0 ' ]);
525+ self ::assertNotNull ($ active , 'active stable row must survive DELETE_BEFORE ' );
526+ self ::assertNull ($ active ->getSoftDeletedAt ());
527+ self ::assertSame ('abcdef1234567890 ' , $ active ->getSource ()['reference ' ] ?? null );
528+
529+ $ reloadedSoftDeleted = $ versionRepo ->findOneBy (['name ' => 'test/pkg ' , 'normalizedVersion ' => '1.1.0.0 ' ]);
530+ self ::assertNotNull ($ reloadedSoftDeleted , 'maintainer-soft-deleted stable row must survive DELETE_BEFORE ' );
531+ self ::assertNotNull ($ reloadedSoftDeleted ->getSoftDeletedAt (), 'soft-delete marker must stay ' );
532+ self ::assertSame (VersionDeletionReason::DeletedByMaintainer, $ reloadedSoftDeleted ->getDeletionReason ());
533+
534+ $ dev = $ versionRepo ->findOneBy (['name ' => 'test/pkg ' , 'normalizedVersion ' => 'dev-main ' ]);
535+ self ::assertNull ($ dev , 'dev row must be hard-deleted by DELETE_BEFORE ' );
536+
537+ self ::assertSame (0 , $ this ->countAudits (AuditRecordType::VersionReferenceChangeBlocked));
538+ }
539+
494540 public function testAutoSoftDeletedDevVersionIsRecoveredWhenBranchReappearsWithUnchangedRef (): void
495541 {
496542 $ existing = $ this ->seedDevVersion ($ this ->package , 'dev-main ' , 'dev-main ' , 'sameref1234567890 ' );
0 commit comments