@@ -855,7 +855,8 @@ void testRereleasePublishedVersion_ShouldCloneFilesAndSubmitForReview() throws E
855855 "1.2.3" ,
856856 "1.2.4" ,
857857 publisherId ,
858- Map .of (skill .getNamespaceId (), com .iflytek .skillhub .domain .namespace .NamespaceRole .OWNER )
858+ Map .of (skill .getNamespaceId (), com .iflytek .skillhub .domain .namespace .NamespaceRole .OWNER ),
859+ false
859860 );
860861
861862 assertEquals ("1.2.4" , result .version ().getVersion ());
@@ -892,7 +893,8 @@ void testRereleasePublishedVersion_ShouldRejectDuplicateTargetVersion() throws E
892893 "1.2.3" ,
893894 "1.2.4" ,
894895 publisherId ,
895- Map .of (skill .getNamespaceId (), com .iflytek .skillhub .domain .namespace .NamespaceRole .OWNER )
896+ Map .of (skill .getNamespaceId (), com .iflytek .skillhub .domain .namespace .NamespaceRole .OWNER ),
897+ false
896898 ));
897899 }
898900
@@ -953,7 +955,8 @@ void testRereleasePublishedVersion_PrivateSkill_ShouldGoToUploaded() throws Exce
953955 "1.2.3" ,
954956 "1.2.4" ,
955957 publisherId ,
956- Map .of (skill .getNamespaceId (), com .iflytek .skillhub .domain .namespace .NamespaceRole .OWNER )
958+ Map .of (skill .getNamespaceId (), com .iflytek .skillhub .domain .namespace .NamespaceRole .OWNER ),
959+ false
957960 );
958961
959962 assertEquals ("1.2.4" , result .version ().getVersion ());
@@ -966,6 +969,100 @@ void testRereleasePublishedVersion_PrivateSkill_ShouldGoToUploaded() throws Exce
966969 assertEquals (30L , skill .getLatestVersionId ());
967970 }
968971
972+ @ Test
973+ void testRereleasePublishedVersion_ShouldRequireConfirmationWhenWarningsExist () throws Exception {
974+ String publisherId = "user-100" ;
975+ Skill skill = new Skill (1L , "demo-skill" , publisherId , SkillVisibility .PUBLIC );
976+ setId (skill , 11L );
977+ skill .setDisplayName ("Demo Skill" );
978+ skill .setSummary ("Original summary" );
979+ Namespace namespace = new Namespace ("global" , "Global" , "owner" );
980+ setId (namespace , 1L );
981+
982+ SkillVersion sourceVersion = new SkillVersion (skill .getId (), "1.2.3" , publisherId );
983+ setId (sourceVersion , 21L );
984+ sourceVersion .setStatus (SkillVersionStatus .PUBLISHED );
985+ sourceVersion .setPublishedAt (Instant .parse ("2026-03-15T10:00:00Z" ));
986+
987+ String sourceSkillMd = "---\n name: Demo Skill\n description: Original summary\n version: 1.2.3\n ---\n Hello world" ;
988+ SkillFile skillMdFile = new SkillFile (sourceVersion .getId (), "SKILL.md" , (long ) sourceSkillMd .getBytes (StandardCharsets .UTF_8 ).length , "text/markdown" , "hash1" , "skills/11/21/SKILL.md" );
989+ SkillMetadata rereleaseMetadata = new SkillMetadata (
990+ "Demo Skill" , "Original summary" , "1.2.4" , "Hello world" ,
991+ Map .of ("name" , "Demo Skill" , "description" , "Original summary" , "version" , "1.2.4" ));
992+
993+ when (skillRepository .findById (skill .getId ())).thenReturn (Optional .of (skill ));
994+ when (namespaceRepository .findById (skill .getNamespaceId ())).thenReturn (Optional .of (namespace ));
995+ when (namespaceRepository .findBySlug ("global" )).thenReturn (Optional .of (namespace ));
996+ when (skillVersionRepository .findBySkillIdAndVersion (skill .getId (), "1.2.3" )).thenReturn (Optional .of (sourceVersion ));
997+ when (skillVersionRepository .findBySkillIdAndVersion (skill .getId (), "1.2.4" )).thenReturn (Optional .empty ());
998+ when (skillFileRepository .findByVersionId (sourceVersion .getId ())).thenReturn (List .of (skillMdFile ));
999+ when (objectStorageService .getObject (skillMdFile .getStorageKey ())).thenReturn (new java .io .ByteArrayInputStream (sourceSkillMd .getBytes (StandardCharsets .UTF_8 )));
1000+ when (skillPackageValidator .validate (anyList ())).thenReturn (ValidationResult .pass ());
1001+ when (skillMetadataParser .parse (anyString ())).thenReturn (rereleaseMetadata );
1002+ when (prePublishValidator .validate (any ())).thenReturn (ValidationResult .warn (List .of (
1003+ "SKILL.md line 5 contains a value that looks like a secret or token." )));
1004+
1005+ DomainBadRequestException exception = assertThrows (DomainBadRequestException .class , () -> service .rereleasePublishedVersion (
1006+ skill .getId (), "1.2.3" , "1.2.4" , publisherId ,
1007+ Map .of (skill .getNamespaceId (), com .iflytek .skillhub .domain .namespace .NamespaceRole .OWNER ),
1008+ false
1009+ ));
1010+
1011+ assertEquals ("error.skill.publish.precheck.confirmRequired" , exception .messageCode ());
1012+ assertTrue (String .valueOf (exception .messageArgs ()[0 ]).contains ("looks like a secret or token" ));
1013+ verify (skillVersionRepository , never ()).save (any (SkillVersion .class ));
1014+ }
1015+
1016+ @ Test
1017+ void testRereleasePublishedVersion_ShouldSucceedWhenWarningsConfirmed () throws Exception {
1018+ String publisherId = "user-100" ;
1019+ Skill skill = new Skill (1L , "demo-skill" , publisherId , SkillVisibility .PUBLIC );
1020+ setId (skill , 11L );
1021+ skill .setDisplayName ("Demo Skill" );
1022+ skill .setSummary ("Original summary" );
1023+ Namespace namespace = new Namespace ("global" , "Global" , "owner" );
1024+ setId (namespace , 1L );
1025+
1026+ SkillVersion sourceVersion = new SkillVersion (skill .getId (), "1.2.3" , publisherId );
1027+ setId (sourceVersion , 21L );
1028+ sourceVersion .setStatus (SkillVersionStatus .PUBLISHED );
1029+ sourceVersion .setPublishedAt (Instant .parse ("2026-03-15T10:00:00Z" ));
1030+
1031+ String sourceSkillMd = "---\n name: Demo Skill\n description: Original summary\n version: 1.2.3\n ---\n Hello world" ;
1032+ SkillFile skillMdFile = new SkillFile (sourceVersion .getId (), "SKILL.md" , (long ) sourceSkillMd .getBytes (StandardCharsets .UTF_8 ).length , "text/markdown" , "hash1" , "skills/11/21/SKILL.md" );
1033+ SkillMetadata rereleaseMetadata = new SkillMetadata (
1034+ "Demo Skill" , "Original summary" , "1.2.4" , "Hello world" ,
1035+ Map .of ("name" , "Demo Skill" , "description" , "Original summary" , "version" , "1.2.4" ));
1036+
1037+ when (skillRepository .findById (skill .getId ())).thenReturn (Optional .of (skill ));
1038+ when (namespaceRepository .findById (skill .getNamespaceId ())).thenReturn (Optional .of (namespace ));
1039+ when (namespaceRepository .findBySlug ("global" )).thenReturn (Optional .of (namespace ));
1040+ when (skillVersionRepository .findBySkillIdAndVersion (skill .getId (), "1.2.3" )).thenReturn (Optional .of (sourceVersion ));
1041+ when (skillVersionRepository .findBySkillIdAndVersion (skill .getId (), "1.2.4" )).thenReturn (Optional .empty ());
1042+ when (skillFileRepository .findByVersionId (sourceVersion .getId ())).thenReturn (List .of (skillMdFile ));
1043+ when (objectStorageService .getObject (skillMdFile .getStorageKey ())).thenReturn (new java .io .ByteArrayInputStream (sourceSkillMd .getBytes (StandardCharsets .UTF_8 )));
1044+ when (skillPackageValidator .validate (anyList ())).thenReturn (ValidationResult .pass ());
1045+ when (skillMetadataParser .parse (anyString ())).thenReturn (rereleaseMetadata );
1046+ when (prePublishValidator .validate (any ())).thenReturn (ValidationResult .warn (List .of (
1047+ "SKILL.md line 5 contains a value that looks like a secret or token." )));
1048+ when (skillVersionRepository .save (any (SkillVersion .class ))).thenAnswer (invocation -> {
1049+ SkillVersion saved = invocation .getArgument (0 );
1050+ if (saved .getId () == null ) { setId (saved , 30L ); }
1051+ return saved ;
1052+ });
1053+ when (skillRepository .save (any ())).thenReturn (skill );
1054+
1055+ SkillPublishService .PublishResult result = service .rereleasePublishedVersion (
1056+ skill .getId (), "1.2.3" , "1.2.4" , publisherId ,
1057+ Map .of (skill .getNamespaceId (), com .iflytek .skillhub .domain .namespace .NamespaceRole .OWNER ),
1058+ true // confirmWarnings = true → should bypass warning and succeed
1059+ );
1060+
1061+ assertEquals ("1.2.4" , result .version ().getVersion ());
1062+ assertEquals (SkillVersionStatus .PENDING_REVIEW , result .version ().getStatus ());
1063+ verify (skillVersionRepository , atLeastOnce ()).save (any (SkillVersion .class ));
1064+ }
1065+
9691066 @ Test
9701067 void testPublishFromEntries_ShouldRejectWhenOtherOwnerHasPublishedSkill () throws Exception {
9711068 String namespaceSlug = "test-ns" ;
@@ -999,6 +1096,39 @@ void testPublishFromEntries_ShouldRejectWhenOtherOwnerHasPublishedSkill() throws
9991096 ));
10001097 }
10011098
1099+ @ Test
1100+ void testPublishFromEntries_ShouldRejectWithPrivateConflictWhenOtherOwnerHasPrivatePublishedSkill () throws Exception {
1101+ String namespaceSlug = "test-ns" ;
1102+ String publisherId = "user-200" ;
1103+ String skillMdContent = "---\n name: test-skill\n description: Test\n version: 1.0.0\n ---\n Body" ;
1104+
1105+ PackageEntry skillMd = new PackageEntry ("SKILL.md" , skillMdContent .getBytes (), skillMdContent .length (), "text/markdown" );
1106+ List <PackageEntry > entries = List .of (skillMd );
1107+
1108+ Namespace namespace = new Namespace (namespaceSlug , "Test NS" , "user-1" );
1109+ setId (namespace , 1L );
1110+ NamespaceMember member = mock (NamespaceMember .class );
1111+ SkillMetadata metadata = new SkillMetadata ("test-skill" , "Test" , "1.0.0" , "Body" , Map .of ());
1112+
1113+ Skill existingSkill = new Skill (1L , "test-skill" , "user-100" , SkillVisibility .PRIVATE );
1114+ setId (existingSkill , 1L );
1115+ SkillVersion publishedVersion = new SkillVersion (1L , "0.1.0" , "user-100" );
1116+ publishedVersion .setStatus (SkillVersionStatus .PUBLISHED );
1117+
1118+ when (namespaceRepository .findBySlug (namespaceSlug )).thenReturn (Optional .of (namespace ));
1119+ when (namespaceMemberRepository .findByNamespaceIdAndUserId (any (), eq (publisherId ))).thenReturn (Optional .of (member ));
1120+ when (skillPackageValidator .validate (entries )).thenReturn (ValidationResult .pass ());
1121+ when (skillMetadataParser .parse (skillMdContent )).thenReturn (metadata );
1122+ when (prePublishValidator .validate (any ())).thenReturn (ValidationResult .pass ());
1123+ when (skillRepository .findByNamespaceIdAndSlug (any (), eq ("test-skill" ))).thenReturn (List .of (existingSkill ));
1124+ when (skillVersionRepository .findBySkillIdAndStatus (1L , SkillVersionStatus .PUBLISHED )).thenReturn (List .of (publishedVersion ));
1125+
1126+ DomainBadRequestException ex = assertThrows (DomainBadRequestException .class , () -> service .publishFromEntries (
1127+ namespaceSlug , entries , publisherId , SkillVisibility .PRIVATE , Set .of ()
1128+ ));
1129+ assertEquals ("error.skill.publish.nameConflict.private" , ex .messageCode ());
1130+ }
1131+
10021132 @ Test
10031133 void testPublishFromEntries_ShouldAllowWhenOtherOwnerHasNonPublishedSkill () throws Exception {
10041134 String namespaceSlug = "test-ns" ;
0 commit comments