test(skill): fix listAvailableSkills arity and cover agent-binding enforcement

This commit is contained in:
matevip 2026-06-07 17:34:43 +08:00
parent 3d8d266e3b
commit 39b6bdc522

View File

@ -2,12 +2,17 @@ package vip.mate.tool.builtin;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.test.util.ReflectionTestUtils;
import vip.mate.agent.context.ChatOrigin;
import vip.mate.llm.routing.AgentBindingResolver;
import vip.mate.skill.runtime.SkillFileAccessPolicy;
import vip.mate.skill.runtime.SkillRuntimeService;
import vip.mate.skill.runtime.model.ResolvedSkill;
import vip.mate.skill.usage.SkillUsageService;
import java.util.List;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@ -30,7 +35,7 @@ class SkillFileToolTest {
skill("ckjia-shopping", "mcp", false),
skill("claude-code", "acp", false)));
String result = tool.listAvailableSkills("code", "acp", "ready", 1);
String result = tool.listAvailableSkills("code", "acp", "ready", 1, null);
assertTrue(result.contains("claude-code"));
assertFalse(result.contains("ckjia-shopping"));
@ -136,6 +141,95 @@ class SkillFileToolTest {
"No truncation banner should appear when caller did not opt into pagination");
}
@Test
@DisplayName("listAvailableSkills hides skills the agent is not bound to")
void listAvailableSkillsRespectsAgentBindings() {
SkillRuntimeService runtimeService = mock(SkillRuntimeService.class);
SkillFileAccessPolicy accessPolicy = mock(SkillFileAccessPolicy.class);
SkillUsageService usageService = mock(SkillUsageService.class);
AgentBindingResolver bindingResolver = mock(AgentBindingResolver.class);
SkillFileTool tool = new SkillFileTool(runtimeService, accessPolicy, usageService);
ReflectionTestUtils.setField(tool, "agentBindingResolver", bindingResolver);
ResolvedSkill bound = skill("alpha-skill", "database", true);
ResolvedSkill unbound = skill("beta-skill", "database", true);
when(runtimeService.getActiveSkills()).thenReturn(List.of(bound, unbound));
// Agent 42 is bound only to alpha-skill; beta-skill must not surface.
when(bindingResolver.getBoundSkillIds(42L)).thenReturn(Set.of(bound.getId()));
ToolContext ctx = ChatOrigin.EMPTY.withAgent(42L).toToolContext();
String result = tool.listAvailableSkills(null, null, null, 20, ctx);
assertTrue(result.contains("alpha-skill"));
assertFalse(result.contains("beta-skill"));
}
@Test
@DisplayName("readSkillFile denies a skill the agent is not bound to")
void readSkillFileDeniesUnboundSkill() {
SkillRuntimeService runtimeService = mock(SkillRuntimeService.class);
SkillFileAccessPolicy accessPolicy = mock(SkillFileAccessPolicy.class);
SkillUsageService usageService = mock(SkillUsageService.class);
AgentBindingResolver bindingResolver = mock(AgentBindingResolver.class);
SkillFileTool tool = new SkillFileTool(runtimeService, accessPolicy, usageService);
ReflectionTestUtils.setField(tool, "agentBindingResolver", bindingResolver);
ResolvedSkill beta = skill("beta-skill", "database", true);
beta.setContent("# Beta\nsecret body");
when(runtimeService.findActiveSkill("beta-skill")).thenReturn(beta);
// Bound to some other skill id, never beta's.
when(bindingResolver.getBoundSkillIds(42L)).thenReturn(Set.of(999L));
ToolContext ctx = ChatOrigin.EMPTY.withAgent(42L).toToolContext();
String result = tool.readSkillFile("beta-skill", "SKILL.md", null, null, ctx);
assertTrue(result.contains("is not available for this agent"));
assertFalse(result.contains("secret body"));
}
@Test
@DisplayName("listSkillFiles denies a skill the agent is not bound to")
void listSkillFilesDeniesUnboundSkill() {
SkillRuntimeService runtimeService = mock(SkillRuntimeService.class);
SkillFileAccessPolicy accessPolicy = mock(SkillFileAccessPolicy.class);
SkillUsageService usageService = mock(SkillUsageService.class);
AgentBindingResolver bindingResolver = mock(AgentBindingResolver.class);
SkillFileTool tool = new SkillFileTool(runtimeService, accessPolicy, usageService);
ReflectionTestUtils.setField(tool, "agentBindingResolver", bindingResolver);
ResolvedSkill beta = skill("beta-skill", "database", true);
when(runtimeService.findActiveSkill("beta-skill")).thenReturn(beta);
when(bindingResolver.getBoundSkillIds(42L)).thenReturn(Set.of(999L));
ToolContext ctx = ChatOrigin.EMPTY.withAgent(42L).toToolContext();
String result = tool.listSkillFiles("beta-skill", ctx);
assertTrue(result.contains("is not available for this agent"));
}
@Test
@DisplayName("agents with no explicit bindings keep full skill access")
void readSkillFileAllowsWhenAgentHasNoBindings() {
SkillRuntimeService runtimeService = mock(SkillRuntimeService.class);
SkillFileAccessPolicy accessPolicy = mock(SkillFileAccessPolicy.class);
SkillUsageService usageService = mock(SkillUsageService.class);
AgentBindingResolver bindingResolver = mock(AgentBindingResolver.class);
SkillFileTool tool = new SkillFileTool(runtimeService, accessPolicy, usageService);
ReflectionTestUtils.setField(tool, "agentBindingResolver", bindingResolver);
ResolvedSkill beta = skill("beta-skill", "database", true);
beta.setContent("# Beta\nvisible body");
when(runtimeService.findActiveSkill("beta-skill")).thenReturn(beta);
// null == no explicit binding restriction inherit every enabled skill.
when(bindingResolver.getBoundSkillIds(42L)).thenReturn(null);
ToolContext ctx = ChatOrigin.EMPTY.withAgent(42L).toToolContext();
String result = tool.readSkillFile("beta-skill", "SKILL.md", null, null, ctx);
assertTrue(result.contains("visible body"));
assertFalse(result.contains("is not available for this agent"));
}
private static ResolvedSkill skill(String name, String source, boolean builtin) {
return ResolvedSkill.builder()
.id((long) name.hashCode())