feat: 完善 Skill 管理与发布治理
- 实现标准资源存储、能力绑定及双格式导入导出 - 接入分类、可见范围、审批发布与资源权限校验 - 补充并发、租户隔离、安全边界和迁移契约测试
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
package tech.easyflow.skill.publish;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.mapper.SkillMapper;
|
||||
import tech.easyflow.skill.service.SkillService;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link SkillApprovalSubjectHandler} 发布候选与已发布快照引用所有权测试。
|
||||
*/
|
||||
public class SkillApprovalSubjectHandlerContentReferenceTest {
|
||||
|
||||
private static final BigInteger SKILL_ID = BigInteger.valueOf(101);
|
||||
private static final BigInteger OPERATOR_ID = BigInteger.valueOf(7);
|
||||
|
||||
private ApprovalInstanceService approvalInstanceService;
|
||||
private SkillService skillService;
|
||||
private SkillMapper skillMapper;
|
||||
private SkillApprovalSubjectHandler handler;
|
||||
private MockedStatic<SaTokenUtil> saToken;
|
||||
|
||||
/**
|
||||
* 初始化审批处理器。
|
||||
*/
|
||||
@Before
|
||||
public void setUp() {
|
||||
approvalInstanceService = mock(ApprovalInstanceService.class);
|
||||
skillService = mock(SkillService.class);
|
||||
skillMapper = mock(SkillMapper.class);
|
||||
LoginAccount account = new LoginAccount();
|
||||
account.setId(OPERATOR_ID);
|
||||
account.setTenantId(BigInteger.ONE);
|
||||
saToken = mockStatic(SaTokenUtil.class);
|
||||
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(account);
|
||||
when(skillMapper.updateApprovalState(any(), any(), any(), any())).thenReturn(1);
|
||||
when(skillMapper.publish(any(), any(), any(), any(), any(), any())).thenReturn(1);
|
||||
handler = new SkillApprovalSubjectHandler(
|
||||
approvalInstanceService,
|
||||
new ObjectMapper(),
|
||||
skillService,
|
||||
skillMapper,
|
||||
mock(ResourceAccessService.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放静态登录态 Mock。
|
||||
*/
|
||||
@After
|
||||
public void tearDown() {
|
||||
saToken.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证发布候选在提交审批请求时立即持有自己的内容引用。
|
||||
*/
|
||||
@Test
|
||||
public void publishCandidateRetainsSnapshotContentsOnSubmit() {
|
||||
Skill draft = skill(PublishStatus.DRAFT, Map.of());
|
||||
Map<String, Object> candidate = snapshot("sha256:candidate");
|
||||
when(skillMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(draft);
|
||||
when(skillService.buildPublishSnapshot(draft)).thenReturn(candidate);
|
||||
|
||||
ApprovalSubmitRequest request = handler.buildSubmitRequest(
|
||||
SKILL_ID, ApprovalActionType.PUBLISH.getCode(), OPERATOR_ID);
|
||||
|
||||
ArgumentCaptor<QueryWrapper> queryCaptor = ArgumentCaptor.forClass(QueryWrapper.class);
|
||||
verify(skillMapper).selectOneByQuery(queryCaptor.capture());
|
||||
assertTrue(queryCaptor.getValue().toSQL().toLowerCase().contains("for update"));
|
||||
verify(skillService).retainSnapshotContents(candidate);
|
||||
assertSame(candidate, request.getSnapshotJson().get("resourceSnapshot"));
|
||||
assertEquals(PublishStatus.DRAFT.getCode(), request.getSnapshotJson().get("previousPublishStatus"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证重新发布时候选引用转为已发布持有,只释放被替换的旧快照。
|
||||
*/
|
||||
@Test
|
||||
public void approvedRepublishReleasesOnlyPreviousPublishedSnapshot() {
|
||||
Map<String, Object> previous = snapshot("sha256:previous");
|
||||
Map<String, Object> candidate = snapshot("sha256:candidate");
|
||||
Skill published = skill(PublishStatus.PUBLISHED, previous);
|
||||
when(skillMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(published);
|
||||
|
||||
handler.applyApprovedAction(
|
||||
ApprovalActionType.PUBLISH.getCode(), SKILL_ID, candidate, OPERATOR_ID);
|
||||
|
||||
verify(skillMapper).publish(
|
||||
eq(SKILL_ID), eq(BigInteger.ONE), same(candidate), any(Date.class),
|
||||
eq(OPERATOR_ID), isNull());
|
||||
verify(skillService).releaseSnapshotContents(previous);
|
||||
verify(skillService, never()).releaseSnapshotContents(candidate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证发布审批驳回或撤回会释放候选快照,且不会释放当前线上快照。
|
||||
*/
|
||||
@Test
|
||||
public void rejectedPublishReleasesCandidateButKeepsPublishedSnapshot() {
|
||||
BigInteger instanceId = BigInteger.valueOf(99);
|
||||
Map<String, Object> publishedSnapshot = snapshot("sha256:published");
|
||||
Map<String, Object> candidate = snapshot("sha256:candidate");
|
||||
Skill published = skill(PublishStatus.PUBLISH_PENDING, publishedSnapshot);
|
||||
published.setCurrentApprovalInstanceId(instanceId);
|
||||
ApprovalInstance instance = new ApprovalInstance();
|
||||
instance.setActionType(ApprovalActionType.PUBLISH.getCode());
|
||||
instance.setSnapshotJson(Map.of("resourceSnapshot", candidate));
|
||||
when(skillMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(published);
|
||||
when(approvalInstanceService.getById(instanceId)).thenReturn(instance);
|
||||
|
||||
handler.restoreState(SKILL_ID, PublishStatus.PUBLISHED);
|
||||
|
||||
verify(skillMapper).updateApprovalState(
|
||||
SKILL_ID, BigInteger.ONE, PublishStatus.PUBLISHED.getCode(), null);
|
||||
verify(skillService).releaseSnapshotContents(candidate);
|
||||
verify(skillService, never()).releaseSnapshotContents(publishedSnapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证删除草稿不触发发布级校验,并使用不含凭据的治理快照。
|
||||
*/
|
||||
@Test
|
||||
public void deleteDraftUsesGovernanceSnapshotWithoutPublishValidation() {
|
||||
Skill draft = skill(PublishStatus.DRAFT, Map.of());
|
||||
Map<String, Object> governance = Map.of(
|
||||
"id", SKILL_ID,
|
||||
"name", "demo-skill",
|
||||
"capabilityCount", 1);
|
||||
when(skillMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(draft);
|
||||
when(skillService.buildGovernanceSnapshot(draft)).thenReturn(governance);
|
||||
|
||||
ApprovalSubmitRequest request = handler.buildSubmitRequest(
|
||||
SKILL_ID, ApprovalActionType.DELETE.getCode(), OPERATOR_ID);
|
||||
|
||||
assertSame(governance, request.getSnapshotJson().get("resourceSnapshot"));
|
||||
verify(skillService).buildGovernanceSnapshot(draft);
|
||||
verify(skillService, never()).buildPublishSnapshot(any());
|
||||
verify(skillService, never()).retainSnapshotContents(any());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证已发布 Skill 仍须先下线,且不会构建任何删除快照。
|
||||
*/
|
||||
@Test
|
||||
public void deletePublishedSkillRequiresOfflineFirst() {
|
||||
Skill published = skill(PublishStatus.PUBLISHED, snapshot("sha256:published"));
|
||||
when(skillMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(published);
|
||||
|
||||
assertThrows(BusinessException.class, () -> handler.buildSubmitRequest(
|
||||
SKILL_ID, ApprovalActionType.DELETE.getCode(), OPERATOR_ID));
|
||||
|
||||
verify(skillService, never()).buildGovernanceSnapshot(any());
|
||||
verify(skillService, never()).buildPublishSnapshot(any());
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除审批通过或无审批直通时必须使用生命周期专用聚合删除入口。
|
||||
*/
|
||||
@Test
|
||||
public void approvedDeleteUsesLifecycleAggregateRemoval() {
|
||||
handler.applyApprovedAction(
|
||||
ApprovalActionType.DELETE.getCode(), SKILL_ID, Map.of(), OPERATOR_ID);
|
||||
|
||||
verify(skillService).removeLifecycleAggregate(SKILL_ID);
|
||||
verify(skillService, never()).removeAggregate(SKILL_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建指定生命周期状态的 Skill。
|
||||
*
|
||||
* @param status 发布状态
|
||||
* @param publishedSnapshot 已发布快照
|
||||
* @return Skill
|
||||
*/
|
||||
private Skill skill(PublishStatus status, Map<String, Object> publishedSnapshot) {
|
||||
Skill skill = new Skill();
|
||||
skill.setId(SKILL_ID);
|
||||
skill.setTenantId(BigInteger.ONE);
|
||||
skill.setName("demo-skill");
|
||||
skill.setDisplayName("Demo Skill");
|
||||
skill.setPublishStatus(status.getCode());
|
||||
skill.setPublishedSnapshotJson(publishedSnapshot);
|
||||
return skill;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建单二进制资源快照。
|
||||
*
|
||||
* @param contentRef 内容引用
|
||||
* @return 快照
|
||||
*/
|
||||
private Map<String, Object> snapshot(String contentRef) {
|
||||
return Map.of("resources", List.of(Map.of(
|
||||
"path", "assets/file.bin",
|
||||
"contentRef", contentRef)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user