test(agent): exercise real capability gate in ProviderRouter primary-selection tests

This commit is contained in:
matevip 2026-06-03 23:39:19 +08:00
parent 5746bcf8cc
commit b22462105c

View File

@ -10,14 +10,17 @@ import vip.mate.llm.model.ModelConfigEntity;
import vip.mate.llm.service.ModelCapabilityService; import vip.mate.llm.service.ModelCapabilityService;
import vip.mate.llm.service.ModelCapabilityService.Modality; import vip.mate.llm.service.ModelCapabilityService.Modality;
import vip.mate.llm.service.ModelConfigService; import vip.mate.llm.service.ModelConfigService;
import vip.mate.skill.manifest.SkillManifest;
import vip.mate.skill.runtime.SkillRuntimeService; import vip.mate.skill.runtime.SkillRuntimeService;
import vip.mate.skill.runtime.model.ResolvedSkill;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@ -45,11 +48,19 @@ class ProviderRouterSelectPrimaryTest {
when(bindingService.getBoundSkillIds(AGENT_ID)).thenReturn(Set.of()); when(bindingService.getBoundSkillIds(AGENT_ID)).thenReturn(Set.of());
} }
private void stubCapabilities(String... needs) { /**
// aggregateModelNeeds reads bound skills return empty so * Bind a single skill that declares the given {@code requires-model}
// resolveRequiredModalities returns null (no capability gate). * tokens, so {@code aggregateModelNeeds} resolves a non-empty capability
// For tests that need capabilities, we stub a bound skill. * set and the capability-gated Pass 1 of {@code selectPrimary} runs.
when(skillRuntimeService.resolveAllSkillsStatus()).thenReturn(List.of()); */
private void bindSkillRequiring(String... needs) {
when(bindingService.getBoundSkillIds(AGENT_ID)).thenReturn(Set.of(1L));
SkillManifest manifest = mock(SkillManifest.class);
when(manifest.getRequiresModel()).thenReturn(List.of(needs));
ResolvedSkill skill = mock(ResolvedSkill.class);
when(skill.getId()).thenReturn(1L);
when(skill.getManifest()).thenReturn(manifest);
when(skillRuntimeService.resolveAllSkillsStatus()).thenReturn(List.of(skill));
} }
// ---- tests ---- // ---- tests ----
@ -71,20 +82,21 @@ class ProviderRouterSelectPrimaryTest {
} }
@Test @Test
@DisplayName("2. Preferred provider satisfying capability wins in pass 1") @DisplayName("2. Preferred provider satisfying the required capability wins in pass 1")
void preferredSatisfyingCapabilityWins() { void preferredSatisfyingCapabilityWins() {
when(bindingService.getBoundSkillIds(AGENT_ID)).thenReturn(Set.of(1L)); bindSkillRequiring("vision");
// No resolved skills aggregateModelNeeds returns empty no capability gate
when(skillRuntimeService.resolveAllSkillsStatus()).thenReturn(List.of());
when(bindingService.getPreferredProviderIds(AGENT_ID)).thenReturn(List.of("deepseek")); when(bindingService.getPreferredProviderIds(AGENT_ID)).thenReturn(List.of("deepseek"));
when(modelConfigService.getDefaultModelByProvider("deepseek")) when(modelConfigService.getDefaultModelByProvider("deepseek"))
.thenReturn(model("deepseek", "deepseek-chat")); .thenReturn(model("deepseek", "deepseek-vl"));
when(capabilityService.resolve(eq("deepseek-vl"), any()))
.thenReturn(EnumSet.of(Modality.VISION));
ModelConfigEntity global = model("openai", "gpt-4o"); ModelConfigEntity global = model("openai", "gpt-4o");
ModelConfigEntity result = router.selectPrimary(AGENT_ID, global); ModelConfigEntity result = router.selectPrimary(AGENT_ID, global);
assertNotNull(result); assertNotNull(result);
assertEquals("deepseek", result.getProvider()); assertEquals("deepseek", result.getProvider());
assertEquals("deepseek-vl", result.getModelName());
} }
@Test @Test
@ -149,4 +161,25 @@ class ProviderRouterSelectPrimaryTest {
ModelConfigEntity result = router.selectPrimary(AGENT_ID, null); ModelConfigEntity result = router.selectPrimary(AGENT_ID, null);
assertNull(result); assertNull(result);
} }
@Test
@DisplayName("8. Preferred misses required capability but global satisfies → global wins in pass 1")
void preferredMissesCapabilityGlobalSatisfies() {
bindSkillRequiring("vision");
when(bindingService.getPreferredProviderIds(AGENT_ID)).thenReturn(List.of("deepseek"));
when(modelConfigService.getDefaultModelByProvider("deepseek"))
.thenReturn(model("deepseek", "deepseek-chat"));
when(capabilityService.resolve(eq("deepseek-chat"), any()))
.thenReturn(EnumSet.noneOf(Modality.class));
ModelConfigEntity global = model("openai", "gpt-4o");
when(capabilityService.resolve(eq("gpt-4o"), any()))
.thenReturn(EnumSet.of(Modality.VISION));
ModelConfigEntity result = router.selectPrimary(AGENT_ID, global);
assertNotNull(result);
assertEquals("openai", result.getProvider());
assertEquals("gpt-4o", result.getModelName());
}
} }