feat(meta): P0b meta-api 用量贡献者端口 + RealMetaImpactFacade 聚合器(替换 Unavailable)

meta-api 新增 4 端口契约:MetaImpactDimension(5维度)/MetaSchemaImpactProbe(草稿+三破坏集)/MetaSchemaDimensionImpact(REAL_COUNT|VERIFIED_ZERO,无 unknown 态)/MetaSchemaUsageContributor(进程内 @Service Bean,各 BC 实现)。

RealMetaImpactFacade(@Primary @Service)替换 UnavailableMetaImpactFacade:破坏判定(G1)与实例计数(各域贡献者)分离——调 G1 组 probe、fan-out 各域、all-real 门(5维度齐全+全非null realityKind;缺维度/重复维度/null 均 fail-closed)、DTO→既有 VO 映射(口径对齐既有 PreviewServiceTest)。退役 Unavailable @Service,单体内仅一个 MetaImpactFacade Bean。

当前 P1/P2/P3 贡献者未接入→注入空 List→缺5维度 fail-closed,等价现状(preview 不放行),待贡献者接入自然真实化(渐进替换、平滑可退)。

验证:单元 14/14(聚合器 5 + 回归 PreviewServiceTest 9/9 mock 不破)。单体启动装配验证进行中。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-20 23:57:32 -07:00
parent a1600d28ce
commit 4bd292f846
7 changed files with 433 additions and 25 deletions

View File

@ -0,0 +1,13 @@
package cn.iocoder.muse.module.meta.api.impact;
/**
* Meta Schema 用量影响的 5 个消费维度( BC owner 实现对应贡献者)
*/
public enum MetaImpactDimension {
PLANNING,
WORK,
KNOWLEDGE_PROJECTION,
AI_CONTEXT,
EXPORT
}

View File

@ -0,0 +1,28 @@
package cn.iocoder.muse.module.meta.api.impact;
/**
* 贡献者Meta 的单维度影响结果
*
* <p> unknown :无法真实作答的贡献者必须抛异常(由聚合器 fail-closed),绝不返回 0 蒙混
* realityKind 区分真实计数已验证真实 0(后者须机械化可证)
*/
public record MetaSchemaDimensionImpact(
MetaImpactDimension dimension,
int affectedInstanceCount,
int fieldRemovedInstanceCount,
int typeChangedInstanceCount,
int visibilityChangedInstanceCount,
RealityKind realityKind) {
public enum RealityKind {
/** 真实查询计数。 */
REAL_COUNT,
/** 已验证本域确无 schema 消费(机械化可证),非 unknown。 */
VERIFIED_ZERO
}
/** 已验证真实 0:本域确不消费 schema,各计数恒 0。 */
public static MetaSchemaDimensionImpact verifiedZero(MetaImpactDimension dimension) {
return new MetaSchemaDimensionImpact(dimension, 0, 0, 0, 0, RealityKind.VERIFIED_ZERO);
}
}

View File

@ -0,0 +1,17 @@
package cn.iocoder.muse.module.meta.api.impact;
import java.util.Set;
/**
* Meta各消费域贡献者的探针:携带草稿标识 + G1 算出的三类破坏字段集
*
* <p>破坏字段集由 meta-server G1 统一算好传入,贡献者只在本域按 fieldKey 计数( BC 边界门,不回查 meta)
*/
public record MetaSchemaImpactProbe(
String schemaKey,
Integer draftVersion,
String draftHash,
Set<String> removedFieldKeys,
Set<String> typeChangedFieldKeys,
Set<String> visibilityChangedFieldKeys) {
}

View File

@ -0,0 +1,16 @@
package cn.iocoder.muse.module.meta.api.impact;
/**
* Meta Schema 用量贡献者端口(进程内 @Service Bean,参照 MuseAccountRecordProjectionApi 先例, @FeignClient)
*
* <p> Meta 定义 owner BC 在本域实现:只读本域 probe 的破坏字段集计数,返回本维度影响
* 无法真实作答抛异常,由聚合器 fail-closed(绝不返回 0 蒙混)
*/
public interface MetaSchemaUsageContributor {
/** 计算本维度受影响实例(只读本域)。 */
MetaSchemaDimensionImpact previewDimensionImpact(MetaSchemaImpactProbe probe);
/** 本贡献者负责的维度(供聚合器校验 5 维度齐全)。 */
MetaImpactDimension dimension();
}

View File

@ -0,0 +1,188 @@
package cn.iocoder.muse.module.meta.application.facade;
import cn.iocoder.muse.framework.common.exception.ServiceException;
import cn.iocoder.muse.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.muse.module.meta.api.impact.MetaImpactDimension;
import cn.iocoder.muse.module.meta.api.impact.MetaSchemaDimensionImpact;
import cn.iocoder.muse.module.meta.api.impact.MetaSchemaImpactProbe;
import cn.iocoder.muse.module.meta.api.impact.MetaSchemaUsageContributor;
import cn.iocoder.muse.module.meta.application.MetaSchemaBreakingFieldSet;
import cn.iocoder.muse.module.meta.application.MetaSchemaBreakingFieldSetProducer;
import cn.iocoder.muse.module.meta.controller.admin.vo.AIContextImpactSummaryVO;
import cn.iocoder.muse.module.meta.controller.admin.vo.ExportImpactSummaryVO;
import cn.iocoder.muse.module.meta.controller.admin.vo.KnowledgeProjectionImpactSummaryVO;
import cn.iocoder.muse.module.meta.controller.admin.vo.PlanningImpactSummaryVO;
import cn.iocoder.muse.module.meta.controller.admin.vo.WorkImpactSummaryVO;
import cn.iocoder.muse.module.meta.dal.dataobject.MetaFieldDO;
import cn.iocoder.muse.module.meta.dal.dataobject.MetaSchemaDO;
import cn.iocoder.muse.module.meta.dal.dataobject.MetaSchemaVersionDO;
import cn.iocoder.muse.module.meta.dal.dataobject.MetaVisibilityPolicyDO;
import cn.iocoder.muse.module.meta.dal.mysql.MetaFieldMapper;
import cn.iocoder.muse.module.meta.dal.mysql.MetaSchemaMapper;
import cn.iocoder.muse.module.meta.dal.mysql.MetaSchemaVersionMapper;
import cn.iocoder.muse.module.meta.dal.mysql.MetaVisibilityPolicyMapper;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static cn.iocoder.muse.module.meta.enums.ErrorCodeConstants.META_EXTERNAL_OWNER_UNAVAILABLE;
import static cn.iocoder.muse.module.meta.enums.ErrorCodeConstants.META_SCHEMA_IDENTITY_CONFLICT;
import static cn.iocoder.muse.module.meta.enums.ErrorCodeConstants.META_SCHEMA_NOT_FOUND;
import static cn.iocoder.muse.module.meta.enums.ErrorCodeConstants.META_SCHEMA_VERSION_NOT_FOUND;
/**
* Meta 影响预览真实聚合器(P0b)
*
* <p>替换 {@code UnavailableMetaImpactFacade}:破坏判定(Meta G1)与实例计数(各消费域贡献者)干净分离
* G1 算出破坏字段集 probe,fan-out 到各 BC 贡献者,贡献者只读本域返回本维度计数,聚合器组装既有 VO
*
* <p><b>all-real (绝不部分放行/不伪造/不降级)</b>: 5 维度贡献者须齐全(缺维度/同维度多实现fail-closed);
* 任一贡献者抛异常 / 返回 null / realityKind整体 fail-closed当前 P1/P2/P3 贡献者未接入注入空 List
* 5 维度fail-closed,等价于现状(preview 不放行),待贡献者接入后自然真实化
*
* <p>{@code @Primary @Service}( @ConditionalOnMissingBean,该注解在 @Service 上不可靠):退役 Unavailable @Service
* 单体内仅此一个 {@link MetaImpactFacade} Bean
*/
@Primary
@Service
public class RealMetaImpactFacade implements MetaImpactFacade {
/** all-real 门要求齐全的 5 个消费维度。 */
private static final Set<MetaImpactDimension> REQUIRED_DIMENSIONS = Set.of(MetaImpactDimension.values());
@Resource
private MetaSchemaMapper schemaMapper;
@Resource
private MetaSchemaVersionMapper schemaVersionMapper;
@Resource
private MetaFieldMapper fieldMapper;
@Resource
private MetaVisibilityPolicyMapper visibilityPolicyMapper;
@Resource
private MetaSchemaBreakingFieldSetProducer breakingFieldSetProducer;
/** 各 BC owner 贡献者(进程内 @Service Bean);未接入时 Spring 注入空 List。 */
@Autowired(required = false)
private List<MetaSchemaUsageContributor> contributors = new ArrayList<>();
@Override
public MetaSchemaImpactSummary previewMetaSchemaDraftImpact(String schemaKey, Integer draftVersion, String draftHash) {
// all-real 1:5 维度贡献者齐全(缺维度/重复fail-closed,绝不部分放行)
Map<MetaImpactDimension, MetaSchemaUsageContributor> byDimension = indexRequiredContributors();
// G1 算破坏字段集 probe(破坏集由 Meta 统一算好传入,贡献者只在本域计数)
MetaSchemaBreakingFieldSet breaking = computeBreakingFieldSet(schemaKey, draftVersion);
MetaSchemaImpactProbe probe = new MetaSchemaImpactProbe(schemaKey, draftVersion, draftHash,
breaking.removedFieldKeys(), breaking.typeChangedFieldKeys(), breaking.visibilityChangedFieldKeys());
// fan-out 各维度;任一异常/返回 null/ realityKind fail-closed(不吞/不降级/不伪造)
Map<MetaImpactDimension, MetaSchemaDimensionImpact> impacts = new EnumMap<>(MetaImpactDimension.class);
for (MetaImpactDimension dimension : MetaImpactDimension.values()) {
MetaSchemaDimensionImpact impact = byDimension.get(dimension).previewDimensionImpact(probe);
if (impact == null || impact.realityKind() == null) {
throw new ServiceException(META_EXTERNAL_OWNER_UNAVAILABLE);
}
impacts.put(dimension, impact);
}
return toSummary(impacts);
}
/** 索引贡献者并校验恰好 5 维度齐全(维度为空 / 同维度重复 / 缺维度均 fail-closed)。 */
private Map<MetaImpactDimension, MetaSchemaUsageContributor> indexRequiredContributors() {
Map<MetaImpactDimension, MetaSchemaUsageContributor> byDimension = new EnumMap<>(MetaImpactDimension.class);
for (MetaSchemaUsageContributor contributor : contributors) {
MetaImpactDimension dimension = contributor.dimension();
if (dimension == null || byDimension.put(dimension, contributor) != null) {
// 维度为空 or 同维度重复贡献者 装配异常,fail-closed(防数据错乱)
throw new ServiceException(META_EXTERNAL_OWNER_UNAVAILABLE);
}
}
if (!byDimension.keySet().containsAll(REQUIRED_DIMENSIONS)) {
// 5 维度未齐 fail-closed(回到现状:preview 不放行)
throw new ServiceException(META_EXTERNAL_OWNER_UNAVAILABLE);
}
return byDimension;
}
/** 查 active/draft 字段与可见性快照,调 G1 产出破坏字段集。 */
private MetaSchemaBreakingFieldSet computeBreakingFieldSet(String schemaKey, Integer draftVersion) {
MetaSchemaDO schema = requireUniqueSchema(schemaKey);
MetaSchemaVersionDO draft = schemaVersionMapper.selectByVersion(schemaKey, draftVersion);
if (draft == null) {
throw new ServiceException(META_SCHEMA_VERSION_NOT_FOUND);
}
MetaSchemaVersionDO active = schemaVersionMapper.selectActiveBySchemaId(schema.getId());
List<MetaFieldDO> activeFields = active == null ? List.of() : fieldMapper.selectListByVersionId(active.getId());
List<MetaFieldDO> draftFields = fieldMapper.selectListByVersionId(draft.getId());
String activeSnapshot = active == null ? null : policySnapshot(active.getId());
String draftSnapshot = policySnapshot(draft.getId());
return breakingFieldSetProducer.produce(activeFields, draftFields, activeSnapshot, draftSnapshot);
}
private String policySnapshot(Long versionId) {
MetaVisibilityPolicyDO policy = visibilityPolicyMapper.selectByVersionId(versionId);
return policy == null ? null : policy.getPolicySnapshot();
}
private MetaSchemaDO requireUniqueSchema(String schemaKey) {
List<MetaSchemaDO> schemas = schemaMapper.selectList(new LambdaQueryWrapperX<MetaSchemaDO>()
.eq(MetaSchemaDO::getSchemaKey, schemaKey));
if (schemas == null || schemas.isEmpty()) {
throw new ServiceException(META_SCHEMA_NOT_FOUND);
}
if (schemas.size() > 1) {
throw new ServiceException(META_SCHEMA_IDENTITY_CONFLICT);
}
return schemas.getFirst();
}
/** DTO→既有 VO 映射(口径与 MetaSchemaImpactPreviewServiceTest 既有断言一致)。 */
private MetaSchemaImpactSummary toSummary(Map<MetaImpactDimension, MetaSchemaDimensionImpact> impacts) {
MetaSchemaDimensionImpact work = impacts.get(MetaImpactDimension.WORK);
MetaSchemaDimensionImpact planning = impacts.get(MetaImpactDimension.PLANNING);
MetaSchemaDimensionImpact knowledge = impacts.get(MetaImpactDimension.KNOWLEDGE_PROJECTION);
MetaSchemaDimensionImpact ai = impacts.get(MetaImpactDimension.AI_CONTEXT);
MetaSchemaDimensionImpact export = impacts.get(MetaImpactDimension.EXPORT);
WorkImpactSummaryVO workVO = new WorkImpactSummaryVO();
workVO.setAffectedWorkCount(work.affectedInstanceCount());
workVO.setAffectedDynamicFieldWorkCount(work.affectedInstanceCount());
workVO.setWorksWithDeprecatedFields(breakingTotal(work));
PlanningImpactSummaryVO planningVO = new PlanningImpactSummaryVO();
planningVO.setAffectedPlanningSections(planning.affectedInstanceCount());
planningVO.setPlanningUsingDeprecatedFields(breakingTotal(planning));
KnowledgeProjectionImpactSummaryVO knowledgeVO = new KnowledgeProjectionImpactSummaryVO();
knowledgeVO.setAffectedProjections(knowledge.affectedInstanceCount());
knowledgeVO.setProjectionsNeedingRebuild(
knowledge.fieldRemovedInstanceCount() + knowledge.typeChangedInstanceCount());
knowledgeVO.setProjectionsWithVisibilityChange(knowledge.visibilityChangedInstanceCount());
AIContextImpactSummaryVO aiVO = new AIContextImpactSummaryVO();
aiVO.setAffectedAIContexts(ai.affectedInstanceCount());
aiVO.setContextsWithFieldRemoved(ai.fieldRemovedInstanceCount());
aiVO.setContextsWithVisibilityChange(ai.visibilityChangedInstanceCount());
ExportImpactSummaryVO exportVO = new ExportImpactSummaryVO();
exportVO.setAffectedExportPolicies(export.affectedInstanceCount());
exportVO.setExportsWithFieldRemoved(export.fieldRemovedInstanceCount());
exportVO.setExportsWithVisibilityChange(export.visibilityChangedInstanceCount());
// totalAffectedProjectionCount 口径 = KnowledgeProjection 受影响投影数(与既有测试断言一致)
return new MetaSchemaImpactSummary(workVO, planningVO, knowledgeVO, aiVO, exportVO,
knowledge.affectedInstanceCount());
}
/** 单维度三类破坏实例之和(供 worksWithDeprecatedFields / planningUsingDeprecatedFields 口径)。 */
private int breakingTotal(MetaSchemaDimensionImpact impact) {
return impact.fieldRemovedInstanceCount() + impact.typeChangedInstanceCount()
+ impact.visibilityChangedInstanceCount();
}
}

View File

@ -1,25 +0,0 @@
package cn.iocoder.muse.module.meta.application.facade;
import cn.iocoder.muse.framework.common.exception.ServiceException;
import org.springframework.stereotype.Service;
import static cn.iocoder.muse.module.meta.enums.ErrorCodeConstants.META_EXTERNAL_OWNER_UNAVAILABLE;
/**
* 默认 Meta 影响 facade
*
* <p>P1R-2 Task 5 不能伪造 Content / Knowledge / AI / Export 的影响数量在真实 owner facade 接入前
* 统一返回业务不可用错误由调用方以 CommonResult code/data/msg 透出</p>
*/
// 单体装配下去除 @ConditionalOnMissingBean(@Service 上不可靠,曾致单体启动 MetaImpactFacade 不注册):
// 全仓仅此一个实现,无真实 impl 竞争,直接 @Service 总注册;语义仍 fail-closed( unavailable)不变
@Service
public class UnavailableMetaImpactFacade implements MetaImpactFacade {
@Override
public MetaSchemaImpactSummary previewMetaSchemaDraftImpact(String schemaKey, Integer draftVersion,
String draftHash) {
throw new ServiceException(META_EXTERNAL_OWNER_UNAVAILABLE);
}
}

View File

@ -0,0 +1,171 @@
package cn.iocoder.muse.module.meta.application.facade;
import cn.iocoder.muse.framework.common.exception.ServiceException;
import cn.iocoder.muse.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.muse.module.meta.api.impact.MetaImpactDimension;
import cn.iocoder.muse.module.meta.api.impact.MetaSchemaDimensionImpact;
import cn.iocoder.muse.module.meta.api.impact.MetaSchemaImpactProbe;
import cn.iocoder.muse.module.meta.api.impact.MetaSchemaUsageContributor;
import cn.iocoder.muse.module.meta.application.MetaSchemaBreakingFieldSet;
import cn.iocoder.muse.module.meta.application.MetaSchemaBreakingFieldSetProducer;
import cn.iocoder.muse.module.meta.dal.dataobject.MetaSchemaDO;
import cn.iocoder.muse.module.meta.dal.dataobject.MetaSchemaVersionDO;
import cn.iocoder.muse.module.meta.dal.mysql.MetaFieldMapper;
import cn.iocoder.muse.module.meta.dal.mysql.MetaSchemaMapper;
import cn.iocoder.muse.module.meta.dal.mysql.MetaSchemaVersionMapper;
import cn.iocoder.muse.module.meta.dal.mysql.MetaVisibilityPolicyMapper;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import java.util.Set;
import static cn.iocoder.muse.module.meta.enums.ErrorCodeConstants.META_EXTERNAL_OWNER_UNAVAILABLE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
/**
* 真实聚合器测试:5 维度齐全正确聚合 + DTOVO 映射口径;缺维度/重复维度/null realityKind fail-closed
*/
class RealMetaImpactFacadeTest extends BaseMockitoUnitTest {
@InjectMocks
private RealMetaImpactFacade facade;
@Mock
private MetaSchemaMapper schemaMapper;
@Mock
private MetaSchemaVersionMapper schemaVersionMapper;
@Mock
private MetaFieldMapper fieldMapper;
@Mock
private MetaVisibilityPolicyMapper visibilityPolicyMapper;
@Mock
private MetaSchemaBreakingFieldSetProducer breakingFieldSetProducer;
@Test
void should_aggregate5Dimensions_andMapBreakingCounts_whenAllReal() {
stubSchemaLookup();
// Planning 真实:7 受影响,破坏 1+0+2;KnowledgeProjection 11 受影响,破坏 3+0+1
setContributors(List.of(
stub(MetaImpactDimension.PLANNING, new MetaSchemaDimensionImpact(
MetaImpactDimension.PLANNING, 7, 1, 0, 2, MetaSchemaDimensionImpact.RealityKind.REAL_COUNT)),
stub(MetaImpactDimension.WORK, MetaSchemaDimensionImpact.verifiedZero(MetaImpactDimension.WORK)),
stub(MetaImpactDimension.KNOWLEDGE_PROJECTION, new MetaSchemaDimensionImpact(
MetaImpactDimension.KNOWLEDGE_PROJECTION, 11, 3, 0, 1, MetaSchemaDimensionImpact.RealityKind.REAL_COUNT)),
stub(MetaImpactDimension.AI_CONTEXT, MetaSchemaDimensionImpact.verifiedZero(MetaImpactDimension.AI_CONTEXT)),
stub(MetaImpactDimension.EXPORT, MetaSchemaDimensionImpact.verifiedZero(MetaImpactDimension.EXPORT))));
MetaImpactFacade.MetaSchemaImpactSummary summary =
facade.previewMetaSchemaDraftImpact("work_core", 3, "hash");
assertEquals(7, summary.planningImpact().getAffectedPlanningSections());
// planningUsingDeprecatedFields = fieldRemoved + typeChanged + visibilityChanged = 1+0+2
assertEquals(3, summary.planningImpact().getPlanningUsingDeprecatedFields());
assertEquals(11, summary.knowledgeProjectionImpact().getAffectedProjections());
// projectionsNeedingRebuild = fieldRemoved + typeChanged = 3+0;visibilityChange = 1
assertEquals(3, summary.knowledgeProjectionImpact().getProjectionsNeedingRebuild());
assertEquals(1, summary.knowledgeProjectionImpact().getProjectionsWithVisibilityChange());
// VERIFIED_ZERO 维度全 0
assertEquals(0, summary.workImpact().getAffectedWorkCount());
assertEquals(0, summary.aiContextImpact().getAffectedAIContexts());
assertEquals(0, summary.exportImpact().getAffectedExportPolicies());
// totalAffectedProjectionCount = KnowledgeProjection 受影响投影数
assertEquals(11, summary.totalAffectedProjectionCount());
}
@Test
void should_failClosed_whenDimensionMissing() {
// 4 维度( EXPORT) all-real fail-closed
setContributors(List.of(
stub(MetaImpactDimension.PLANNING, MetaSchemaDimensionImpact.verifiedZero(MetaImpactDimension.PLANNING)),
stub(MetaImpactDimension.WORK, MetaSchemaDimensionImpact.verifiedZero(MetaImpactDimension.WORK)),
stub(MetaImpactDimension.KNOWLEDGE_PROJECTION, MetaSchemaDimensionImpact.verifiedZero(MetaImpactDimension.KNOWLEDGE_PROJECTION)),
stub(MetaImpactDimension.AI_CONTEXT, MetaSchemaDimensionImpact.verifiedZero(MetaImpactDimension.AI_CONTEXT))));
ServiceException ex = assertThrows(ServiceException.class,
() -> facade.previewMetaSchemaDraftImpact("work_core", 3, "hash"));
assertEquals(META_EXTERNAL_OWNER_UNAVAILABLE.getCode(), ex.getCode());
}
@Test
void should_failClosed_whenNoContributors() {
// List(P1/P2/P3 未接入的当前态) fail-closed,等价现状
setContributors(List.of());
ServiceException ex = assertThrows(ServiceException.class,
() -> facade.previewMetaSchemaDraftImpact("work_core", 3, "hash"));
assertEquals(META_EXTERNAL_OWNER_UNAVAILABLE.getCode(), ex.getCode());
}
@Test
void should_failClosed_whenDuplicateDimension() {
// 同维度两个贡献者 装配错乱,fail-closed
setContributors(List.of(
stub(MetaImpactDimension.PLANNING, MetaSchemaDimensionImpact.verifiedZero(MetaImpactDimension.PLANNING)),
stub(MetaImpactDimension.PLANNING, MetaSchemaDimensionImpact.verifiedZero(MetaImpactDimension.PLANNING)),
stub(MetaImpactDimension.WORK, MetaSchemaDimensionImpact.verifiedZero(MetaImpactDimension.WORK)),
stub(MetaImpactDimension.KNOWLEDGE_PROJECTION, MetaSchemaDimensionImpact.verifiedZero(MetaImpactDimension.KNOWLEDGE_PROJECTION)),
stub(MetaImpactDimension.AI_CONTEXT, MetaSchemaDimensionImpact.verifiedZero(MetaImpactDimension.AI_CONTEXT)),
stub(MetaImpactDimension.EXPORT, MetaSchemaDimensionImpact.verifiedZero(MetaImpactDimension.EXPORT))));
ServiceException ex = assertThrows(ServiceException.class,
() -> facade.previewMetaSchemaDraftImpact("work_core", 3, "hash"));
assertEquals(META_EXTERNAL_OWNER_UNAVAILABLE.getCode(), ex.getCode());
}
@Test
void should_failClosed_whenContributorReturnsNullRealityKind() {
stubSchemaLookup();
// 某贡献者返回无 realityKind(unknown 蒙混) fail-closed,绝不放行
setContributors(List.of(
stub(MetaImpactDimension.PLANNING, new MetaSchemaDimensionImpact(
MetaImpactDimension.PLANNING, 1, 0, 0, 0, null)),
stub(MetaImpactDimension.WORK, MetaSchemaDimensionImpact.verifiedZero(MetaImpactDimension.WORK)),
stub(MetaImpactDimension.KNOWLEDGE_PROJECTION, MetaSchemaDimensionImpact.verifiedZero(MetaImpactDimension.KNOWLEDGE_PROJECTION)),
stub(MetaImpactDimension.AI_CONTEXT, MetaSchemaDimensionImpact.verifiedZero(MetaImpactDimension.AI_CONTEXT)),
stub(MetaImpactDimension.EXPORT, MetaSchemaDimensionImpact.verifiedZero(MetaImpactDimension.EXPORT))));
ServiceException ex = assertThrows(ServiceException.class,
() -> facade.previewMetaSchemaDraftImpact("work_core", 3, "hash"));
assertEquals(META_EXTERNAL_OWNER_UNAVAILABLE.getCode(), ex.getCode());
}
private void stubSchemaLookup() {
MetaSchemaDO schema = new MetaSchemaDO();
schema.setId(1L);
schema.setSchemaKey("work_core");
MetaSchemaVersionDO draft = new MetaSchemaVersionDO();
draft.setId(12L);
lenient().when(schemaMapper.selectList(any())).thenReturn(List.of(schema));
lenient().when(schemaVersionMapper.selectByVersion("work_core", 3)).thenReturn(draft);
lenient().when(schemaVersionMapper.selectActiveBySchemaId(1L)).thenReturn(null);
lenient().when(fieldMapper.selectListByVersionId(anyLong())).thenReturn(List.of());
lenient().when(visibilityPolicyMapper.selectByVersionId(anyLong())).thenReturn(null);
lenient().when(breakingFieldSetProducer.produce(any(), any(), any(), any()))
.thenReturn(new MetaSchemaBreakingFieldSet(Set.of(), Set.of(), Set.of()));
}
private void setContributors(List<MetaSchemaUsageContributor> contributors) {
ReflectionTestUtils.setField(facade, "contributors", contributors);
}
private static MetaSchemaUsageContributor stub(MetaImpactDimension dimension, MetaSchemaDimensionImpact impact) {
return new MetaSchemaUsageContributor() {
@Override
public MetaSchemaDimensionImpact previewDimensionImpact(MetaSchemaImpactProbe probe) {
return impact;
}
@Override
public MetaImpactDimension dimension() {
return dimension;
}
};
}
}