diff --git a/docs/07-skill-protocol.md b/docs/07-skill-protocol.md index c870bf68..f9a2cc30 100644 --- a/docs/07-skill-protocol.md +++ b/docs/07-skill-protocol.md @@ -41,6 +41,7 @@ description: When to use # 必需,1-2 句话 - `name` 和 `description` 为必需字段,缺失则校验失败 - `name` 映射为 `skill.slug`(首次发布时),后续版本不可变更 - 可选的 `metadata.display-name` 映射为 `skill.display_name`,不参与 slug 生成。必须是字符串,去除首尾空白后最多 200 个字符;缺失、null 或空白时回退到 `name`。展示名沿用版本发布/审核流程,待审核更新不覆盖已发布标题;撤下最新版本并回退到旧版本时,标题和摘要同步恢复到该旧版本。 +- 上述类型和长度限制在新上传解析时检查。读取历史版本快照时,非字符串或超长的旧 `display-name` 回退到该版本的 `name`,原始 frontmatter 保留,避免阻塞审核、提升或版本撤回。 - `description` 映射为 `skill.summary` - frontmatter 完整解析结果存入 `skill_version.parsed_metadata_json` diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/bundle/SkillSuiteBundlePackageAnalyzerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/bundle/SkillSuiteBundlePackageAnalyzerTest.java index f2ed5d65..3a0b99de 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/bundle/SkillSuiteBundlePackageAnalyzerTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/bundle/SkillSuiteBundlePackageAnalyzerTest.java @@ -61,6 +61,21 @@ class SkillSuiteBundlePackageAnalyzerTest { }); } + @Test + void rejectsInvalidDisplayNamesInNewBundlePackages() throws Exception { + for (String invalidValue : new String[] {"123", "中".repeat(201)}) { + String skill = "---\nname: first\ndescription: Test\nversion: 1.0.0\nmetadata:\n display-name: " + + invalidValue + "\n---\nBody"; + SkillSuiteBundlePackageAnalyzer.BundleAnalysis result = analyzer.analyze(List.of( + entry("SUITE.yaml", manifest(packageMember("first"), "@global/first")), + entry("skills/first/SKILL.md", skill) + )); + + assertThat(result.packageMembers()).singleElement().satisfies(member -> + assertThat(member.validation().passed()).isFalse()); + } + } + @Test void reportsMissingRootSkillMdAndDoesNotPromoteNestedOne() throws Exception { SkillSuiteBundlePackageAnalyzer.BundleAnalysis result = analyzer.analyze(List.of( diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionService.java index 697548f7..0e671ceb 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionService.java @@ -1,5 +1,6 @@ package com.iflytek.skillhub.domain.review; +import com.fasterxml.jackson.databind.ObjectMapper; import com.iflytek.skillhub.domain.audit.AuditDetail; import com.iflytek.skillhub.domain.event.PromotionApprovedEvent; import com.iflytek.skillhub.domain.event.PromotionRejectedEvent; @@ -17,6 +18,7 @@ import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException; import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException; import com.iflytek.skillhub.domain.shared.exception.DomainNotFoundException; import com.iflytek.skillhub.domain.skill.*; +import com.iflytek.skillhub.domain.skill.metadata.SkillMetadata; import jakarta.persistence.EntityManager; import org.springframework.context.ApplicationEventPublisher; import org.springframework.dao.DataIntegrityViolationException; @@ -51,6 +53,7 @@ public class PromotionService { private final GovernanceNotificationService governanceNotificationService; private final EntityManager entityManager; private final Clock clock; + private final ObjectMapper objectMapper; public PromotionService(PromotionRequestRepository promotionRequestRepository, SkillRepository skillRepository, @@ -62,7 +65,8 @@ public class PromotionService { ApplicationEventPublisher eventPublisher, GovernanceNotificationService governanceNotificationService, EntityManager entityManager, - Clock clock) { + Clock clock, + ObjectMapper objectMapper) { this.promotionRequestRepository = promotionRequestRepository; this.skillRepository = skillRepository; this.skillVersionRepository = skillVersionRepository; @@ -74,6 +78,7 @@ public class PromotionService { this.governanceNotificationService = governanceNotificationService; this.entityManager = entityManager; this.clock = clock; + this.objectMapper = objectMapper; } /** @@ -217,8 +222,7 @@ public class PromotionService { // Serialize approval with ordinary uploads that lock target Skill before publishing. entityManager.lock(targetSkill, jakarta.persistence.LockModeType.PESSIMISTIC_WRITE); assertTargetVersionAvailable(targetSkill.getId(), sourceVersion.getVersion()); - targetSkill.setDisplayName(sourceSkill.getDisplayName()); - targetSkill.setSummary(sourceSkill.getSummary()); + applySourceVersionMetadata(targetSkill, sourceSkill, sourceVersion); } else { if (promotionRequestRepository.findActiveInitialBySourceSkillId(sourceSkill.getId()).isPresent()) { throw new DomainBadRequestException("promotion.already_promoted", sourceSkill.getId()); @@ -226,8 +230,7 @@ public class PromotionService { assertTargetSkillNotExists(approvedRequest, sourceSkill); targetSkill = new Skill(approvedRequest.getTargetNamespaceId(), sourceSkill.getSlug(), sourceSkill.getOwnerId(), SkillVisibility.PUBLIC); - targetSkill.setDisplayName(sourceSkill.getDisplayName()); - targetSkill.setSummary(sourceSkill.getSummary()); + applySourceVersionMetadata(targetSkill, sourceSkill, sourceVersion); targetSkill.setSourceSkillId(sourceSkill.getId()); targetSkill.setCreatedBy(reviewerId); targetSkill.setUpdatedBy(reviewerId); @@ -293,6 +296,22 @@ public class PromotionService { return savedRequest; } + private void applySourceVersionMetadata(Skill target, Skill source, SkillVersion sourceVersion) { + String metadataJson = sourceVersion.getParsedMetadataJson(); + if (metadataJson == null || metadataJson.isBlank()) { + target.setDisplayName(source.getDisplayName()); + target.setSummary(source.getSummary()); + return; + } + try { + SkillMetadata metadata = objectMapper.readValue(metadataJson, SkillMetadata.class); + target.setDisplayName(metadata.displayName()); + target.setSummary(metadata.description()); + } catch (Exception e) { + throw new IllegalStateException("Failed to deserialize skill metadata", e); + } + } + private void assertTargetSkillNotExists(PromotionRequest approvedRequest, Skill sourceSkill) { for (Skill existing : skillRepository.findByNamespaceIdAndSlug( approvedRequest.getTargetNamespaceId(), sourceSkill.getSlug())) { diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadata.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadata.java index 45bfffe7..8fb58402 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadata.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadata.java @@ -1,6 +1,5 @@ package com.iflytek.skillhub.domain.skill.metadata; -import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException; import java.util.Map; public record SkillMetadata( @@ -10,11 +9,7 @@ public record SkillMetadata( String body, Map frontmatter ) { - public SkillMetadata { - resolveDisplayName(name, frontmatter); - } - - /** Human-readable label; the package name remains the source of the skill slug. */ + /** Human-readable label; unsupported legacy snapshot values fall back to the package name. */ public String displayName() { return resolveDisplayName(name, frontmatter); } @@ -28,14 +23,14 @@ public record SkillMetadata( return name; } if (!(value instanceof String text)) { - throw new DomainBadRequestException("error.skill.metadata.displayName.invalid"); + return name; } String displayName = text.strip(); if (displayName.isEmpty()) { return name; } if (displayName.length() > 200) { - throw new DomainBadRequestException("error.skill.metadata.displayName.tooLong", 200); + return name; } return displayName; } diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParser.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParser.java index 4c5055b1..af7959db 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParser.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParser.java @@ -59,9 +59,26 @@ public class SkillMetadataParser { version = extractNestedOptionalField(frontmatter, "metadata", "version"); } + validateDisplayName(frontmatter); return new SkillMetadata(name, description, version, body, frontmatter); } + private void validateDisplayName(Map frontmatter) { + if (!(frontmatter.get("metadata") instanceof Map metadata)) { + return; + } + Object value = metadata.get("display-name"); + if (value == null) { + return; + } + if (!(value instanceof String text)) { + throw new DomainBadRequestException("error.skill.metadata.displayName.invalid"); + } + if (text.strip().length() > 200) { + throw new DomainBadRequestException("error.skill.metadata.displayName.tooLong", 200); + } + } + private Map parseFrontmatter(String yamlContent) { try { LoaderOptions loaderOptions = new LoaderOptions(); diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/PromotionServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/PromotionServiceTest.java index 37b040b8..ad7575fa 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/PromotionServiceTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/PromotionServiceTest.java @@ -1,5 +1,6 @@ package com.iflytek.skillhub.domain.review; +import com.fasterxml.jackson.databind.ObjectMapper; import com.iflytek.skillhub.domain.event.SkillPublishedEvent; import com.iflytek.skillhub.domain.governance.GovernanceNotificationService; import com.iflytek.skillhub.domain.namespace.Namespace; @@ -16,6 +17,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -64,7 +67,7 @@ class PromotionServiceTest { promotionService = new PromotionService( promotionRequestRepository, skillRepository, skillVersionRepository, skillFileRepository, namespaceRepository, namespaceMemberRepository, permissionChecker, - eventPublisher, governanceNotificationService, entityManager, CLOCK); + eventPublisher, governanceNotificationService, entityManager, CLOCK, new ObjectMapper()); lenient().when(namespaceRepository.findById(5L)).thenReturn(Optional.of(createSourceNamespace())); } @@ -100,6 +103,13 @@ class PromotionServiceTest { return sv; } + private String selectedVersionMetadata(boolean hasDisplayName) { + return """ + {"name":"old-package-name","description":"Old version summary", + "frontmatter":{"metadata":{"display-name":"%s"}}} + """.formatted(hasDisplayName ? " 旧版标题 " : " "); + } + private Namespace createGlobalNamespace() { Namespace ns = new Namespace("global", "Global", "user-1"); setField(ns, "id", TARGET_NAMESPACE_ID); @@ -655,12 +665,15 @@ class PromotionServiceTest { assertEquals("promotion.target_skill_conflict", ex.messageCode()); } - @Test - void shouldCopyDisplayNameAndSummaryToNewSkill() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void shouldCopySelectedVersionMetadataToNewSkill(boolean hasDisplayName) { PromotionRequest pr = createPendingPromotion(); PromotionRequest approvedRequest = approvedPromotion(pr, "ok"); Skill sourceSkill = createSourceSkill(); SkillVersion sourceVersion = createPublishedVersion(); + sourceSkill.setLatestVersionId(999L); + sourceVersion.setParsedMetadataJson(selectedVersionMetadata(hasDisplayName)); when(promotionRequestRepository.findById(PROMOTION_ID)) .thenReturn(Optional.of(pr), Optional.of(approvedRequest)); @@ -686,8 +699,9 @@ class PromotionServiceTest { ArgumentCaptor skillCaptor = ArgumentCaptor.forClass(Skill.class); verify(skillRepository, times(2)).save(skillCaptor.capture()); Skill newSkill = skillCaptor.getAllValues().get(0); - assertEquals("My Skill", newSkill.getDisplayName()); - assertEquals("A test skill", newSkill.getSummary()); + assertEquals(hasDisplayName ? "旧版标题" : "old-package-name", newSkill.getDisplayName()); + assertEquals("Old version summary", newSkill.getSummary()); + assertEquals(NEW_VERSION_ID, newSkill.getLatestVersionId()); } } @@ -784,8 +798,9 @@ class PromotionServiceTest { assertEquals("2.0.0", state.targetCurrentVersion()); } - @Test - void approvesUpdateWithoutCreatingAnotherGlobalSkill() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void approvesUpdateUsingSelectedVersionMetadata(boolean hasDisplayName) { PromotionRequest pending = createPendingPromotion(); pending.setRequestKind(PromotionRequestKind.UPDATE); pending.setTargetSkillId(NEW_SKILL_ID); @@ -808,8 +823,12 @@ class PromotionServiceTest { PROMOTION_ID, ReviewTaskStatus.APPROVED, REVIEWER_ID, "approved", NEW_SKILL_ID, pending.getVersion())) .thenReturn(1); - when(skillRepository.findById(SOURCE_SKILL_ID)).thenReturn(Optional.of(createSourceSkill())); - when(skillVersionRepository.findById(SOURCE_VERSION_ID)).thenReturn(Optional.of(createPublishedVersion())); + Skill source = createSourceSkill(); + source.setLatestVersionId(998L); + SkillVersion sourceVersion = createPublishedVersion(); + sourceVersion.setParsedMetadataJson(selectedVersionMetadata(hasDisplayName)); + when(skillRepository.findById(SOURCE_SKILL_ID)).thenReturn(Optional.of(source)); + when(skillVersionRepository.findById(SOURCE_VERSION_ID)).thenReturn(Optional.of(sourceVersion)); when(promotionRequestRepository.findActiveInitialBySourceSkillId(SOURCE_SKILL_ID)) .thenReturn(Optional.of(initialLink())); when(skillRepository.findById(NEW_SKILL_ID)).thenReturn(Optional.of(target)); @@ -823,8 +842,8 @@ class PromotionServiceTest { assertEquals(NEW_SKILL_ID, result.getTargetSkillId()); assertEquals(NEW_VERSION_ID, result.getTargetVersionId()); assertEquals(NEW_VERSION_ID, target.getLatestVersionId()); - assertEquals("My Skill", target.getDisplayName()); - assertEquals("A test skill", target.getSummary()); + assertEquals(hasDisplayName ? "旧版标题" : "old-package-name", target.getDisplayName()); + assertEquals("Old version summary", target.getSummary()); verify(promotionRequestRepository).updateStatusWithVersion( PROMOTION_ID, ReviewTaskStatus.APPROVED, REVIEWER_ID, "approved", NEW_SKILL_ID, pending.getVersion()); diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParserTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParserTest.java index 01ae3280..56117482 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParserTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParserTest.java @@ -42,6 +42,25 @@ class SkillMetadataParserTest { assertEquals("old-skill", new SkillMetadata("old-skill", "Test", "1.0.0", "Body", null).displayName()); } + @Test + void readsLegacyDisplayNameSnapshotsWithoutDiscardingRawValues() throws Exception { + var mapper = new com.fasterxml.jackson.databind.ObjectMapper(); + for (Object legacyValue : new Object[] {123, true, java.util.List.of("one", "two"), + Map.of("label", "test"), "中".repeat(201)}) { + String snapshot = mapper.writeValueAsString(Map.of( + "name", "old-skill", "description", "Historical summary", + "frontmatter", Map.of("metadata", Map.of("display-name", legacyValue)))); + + SkillMetadata metadata = mapper.readValue(snapshot, SkillMetadata.class); + + assertEquals("old-skill", metadata.displayName()); + assertEquals(legacyValue, + ((Map) metadata.frontmatter().get("metadata")).get("display-name")); + assertEquals(mapper.readTree(snapshot).get("frontmatter"), + mapper.readTree(mapper.writeValueAsString(metadata)).get("frontmatter")); + } + } + @Test void rejectsNonStringDisplayNames() { for (String value : new String[] {"123", "true", "[one, two]", "{label: test}"}) { diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceServiceTest.java index 88429ae7..7c7a6f03 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceServiceTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceServiceTest.java @@ -255,8 +255,8 @@ class SkillGovernanceServiceTest { } @ParameterizedTest - @ValueSource(booleans = {false, true}) - void yankVersion_restoresLatestPublishedMetadata(boolean hasDisplayName) throws Exception { + @ValueSource(strings = {"missing", "custom", "number", "tooLong"}) + void yankVersion_restoresLatestPublishedMetadata(String displayNameCase) throws Exception { SkillVersion yanked = new SkillVersion(2L, "2.0.0", "owner"); setField(yanked, "id", 22L); yanked.setStatus(SkillVersionStatus.PUBLISHED); @@ -269,9 +269,12 @@ class SkillGovernanceServiceTest { fallback.setParsedMetadataJson(new ObjectMapper().writeValueAsString(Map.of( "name", "example-skill", "description", "Published summary from v1", - "frontmatter", hasDisplayName - ? Map.of("metadata", Map.of("display-name", "示例技能一")) - : Map.of()))); + "frontmatter", switch (displayNameCase) { + case "custom" -> Map.of("metadata", Map.of("display-name", "示例技能一")); + case "number" -> Map.of("metadata", Map.of("display-name", 123)); + case "tooLong" -> Map.of("metadata", Map.of("display-name", "中".repeat(201))); + default -> Map.of(); + }))); Skill skill = new Skill(1L, "demo", "owner", com.iflytek.skillhub.domain.skill.SkillVisibility.PUBLIC); setField(skill, "id", 2L); @@ -288,7 +291,7 @@ class SkillGovernanceServiceTest { service.yankVersion(22L, "admin", "127.0.0.1", "JUnit", "broken"); assertThat(skill.getLatestVersionId()).isEqualTo(11L); - assertThat(skill.getDisplayName()).isEqualTo(hasDisplayName ? "示例技能一" : "example-skill"); + assertThat(skill.getDisplayName()).isEqualTo(displayNameCase.equals("custom") ? "示例技能一" : "example-skill"); assertThat(skill.getSummary()).isEqualTo("Published summary from v1"); verify(skillRepository).save(skill); } diff --git a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/event/SearchIndexEventListener.java b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/event/SearchIndexEventListener.java index b0fcee58..098e1231 100644 --- a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/event/SearchIndexEventListener.java +++ b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/event/SearchIndexEventListener.java @@ -2,6 +2,7 @@ package com.iflytek.skillhub.search.event; import com.iflytek.skillhub.domain.event.SkillPublishedEvent; import com.iflytek.skillhub.domain.event.SkillStatusChangedEvent; +import com.iflytek.skillhub.domain.event.SkillVersionYankedEvent; import com.iflytek.skillhub.domain.skill.SkillStatus; import com.iflytek.skillhub.search.SearchIndexService; import com.iflytek.skillhub.search.SearchRebuildService; @@ -32,6 +33,12 @@ public class SearchIndexEventListener { searchRebuildService.rebuildBySkill(event.skillId()); } + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Async("skillhubEventExecutor") + public void onSkillVersionYanked(SkillVersionYankedEvent event) { + searchRebuildService.rebuildBySkill(event.skillId()); + } + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) @Async("skillhubEventExecutor") public void onSkillStatusChanged(SkillStatusChangedEvent event) { diff --git a/server/skillhub-search/src/test/java/com/iflytek/skillhub/search/event/SearchIndexEventListenerTest.java b/server/skillhub-search/src/test/java/com/iflytek/skillhub/search/event/SearchIndexEventListenerTest.java index 84a2b295..2bfee788 100644 --- a/server/skillhub-search/src/test/java/com/iflytek/skillhub/search/event/SearchIndexEventListenerTest.java +++ b/server/skillhub-search/src/test/java/com/iflytek/skillhub/search/event/SearchIndexEventListenerTest.java @@ -1,14 +1,21 @@ package com.iflytek.skillhub.search.event; import com.iflytek.skillhub.domain.event.SkillPublishedEvent; +import com.iflytek.skillhub.domain.event.SkillVersionYankedEvent; import com.iflytek.skillhub.domain.event.SkillStatusChangedEvent; import com.iflytek.skillhub.domain.skill.SkillStatus; import com.iflytek.skillhub.search.SearchIndexService; import com.iflytek.skillhub.search.SearchRebuildService; import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.transaction.event.TransactionalEventListenerFactory; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionSynchronizationUtils; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; class SearchIndexEventListenerTest { @@ -23,6 +30,41 @@ class SearchIndexEventListenerTest { verify(searchRebuildService).rebuildBySkill(42L); } + @Test + void yankedVersionShouldRebuildOnlyAfterCommit() { + assertYankTransactionOutcome(TransactionSynchronization.STATUS_COMMITTED); + } + + @Test + void rolledBackYankShouldNotRebuildSearchDocument() { + assertYankTransactionOutcome(TransactionSynchronization.STATUS_ROLLED_BACK); + } + + private void assertYankTransactionOutcome(int completionStatus) { + SearchRebuildService rebuildService = mock(SearchRebuildService.class); + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.registerBean(TransactionalEventListenerFactory.class); + context.registerBean(SearchIndexEventListener.class, + () -> new SearchIndexEventListener(rebuildService, mock(SearchIndexService.class))); + context.refresh(); + TransactionSynchronizationManager.setActualTransactionActive(true); + TransactionSynchronizationManager.initSynchronization(); + try { + context.publishEvent(new SkillVersionYankedEvent(42L, 100L, "owner-1", true)); + verifyNoInteractions(rebuildService); + TransactionSynchronizationUtils.triggerAfterCompletion(completionStatus); + if (completionStatus == TransactionSynchronization.STATUS_COMMITTED) { + verify(rebuildService).rebuildBySkill(42L); + } else { + verifyNoInteractions(rebuildService); + } + } finally { + TransactionSynchronizationManager.clearSynchronization(); + TransactionSynchronizationManager.setActualTransactionActive(false); + } + } + } + @Test void archivedStatusShouldRemoveSearchDocument() { SearchRebuildService searchRebuildService = mock(SearchRebuildService.class);