mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(skill): scope agent runtime skill resolution to the conversation workspace
This commit is contained in:
parent
f715e8ffc0
commit
728ed53062
@ -1784,7 +1784,7 @@ public class AgentGraphBuilder {
|
||||
boolean anyHasConstraints = false;
|
||||
for (String skillName : loaded) {
|
||||
try {
|
||||
vip.mate.skill.runtime.model.ResolvedSkill skill = skillRuntimeService.findActiveSkill(skillName);
|
||||
vip.mate.skill.runtime.model.ResolvedSkill skill = skillRuntimeService.findActiveSkill(skillName, workspaceId);
|
||||
if (skill != null && skill.getManifest() != null) {
|
||||
List<String> constraints = skill.getManifest().getConstraints();
|
||||
if (constraints != null && !constraints.isEmpty()) {
|
||||
|
||||
@ -0,0 +1,38 @@
|
||||
package vip.mate.agent.context;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
import vip.mate.approval.grant.WorkspaceLookupCache;
|
||||
|
||||
/**
|
||||
* Resolves the workspace of the currently-executing conversation for the agent
|
||||
* runtime skill-resolution path.
|
||||
* <p>
|
||||
* The {@link ChatOrigin} carried in a tool's {@code ToolContext} usually already
|
||||
* holds the workspaceId (populated at the web / channel entry point). Some paths
|
||||
* — notably approval replay — carry a conversationId but a {@code null}
|
||||
* workspaceId; there we fall back to {@link WorkspaceLookupCache}, which maps a
|
||||
* conversationId to its owning workspace. When neither yields a workspace, the
|
||||
* result is {@code null}: callers must treat that conservatively (resolve only
|
||||
* builtin / global skills, never another workspace's skill).
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AgentWorkspaceResolver {
|
||||
|
||||
private final WorkspaceLookupCache workspaceLookupCache;
|
||||
|
||||
/** Best-effort workspace id for the given origin; {@code null} if unresolved. */
|
||||
@Nullable
|
||||
public Long resolve(@Nullable ChatOrigin origin) {
|
||||
if (origin == null) {
|
||||
return null;
|
||||
}
|
||||
if (origin.workspaceId() != null) {
|
||||
return origin.workspaceId();
|
||||
}
|
||||
String conversationId = origin.conversationId();
|
||||
return conversationId != null ? workspaceLookupCache.resolveByConversation(conversationId) : null;
|
||||
}
|
||||
}
|
||||
@ -594,7 +594,7 @@ public class ToolExecutionExecutor {
|
||||
toolCall.id(), toolName, redirect.response()));
|
||||
continue;
|
||||
}
|
||||
String msg = skillAwareNotFoundMessage(toolName);
|
||||
String msg = skillAwareNotFoundMessage(toolName, safeOrigin);
|
||||
log.warn("[ToolExecutor] {}", msg);
|
||||
events.add(GraphEventPublisher.toolComplete(toolCall.id(), toolName, msg, false));
|
||||
allResponses.add(new ToolResponseMessage.ToolResponse(
|
||||
@ -684,7 +684,7 @@ public class ToolExecutionExecutor {
|
||||
return new ToolResponseMessage.ToolResponse(
|
||||
toolCall.id(), toolName, redirect.response());
|
||||
}
|
||||
String msg = skillAwareNotFoundMessage(toolName);
|
||||
String msg = skillAwareNotFoundMessage(toolName, replayOriginForRedirect);
|
||||
log.warn("[ToolExecutor] Pre-approved {}", msg);
|
||||
events.add(GraphEventPublisher.toolComplete(toolCall.id(), toolName, msg, false));
|
||||
return new ToolResponseMessage.ToolResponse(toolCall.id(), toolName, msg);
|
||||
@ -1276,10 +1276,34 @@ public class ToolExecutionExecutor {
|
||||
return Map.copyOf(result);
|
||||
}
|
||||
|
||||
private String skillAwareNotFoundMessage(String toolName) {
|
||||
/**
|
||||
* Best-effort conversation workspace from a {@link ChatOrigin}, with a
|
||||
* {@code WorkspaceLookupCache} fallback for paths (e.g. approval replay)
|
||||
* that carry a conversationId but no workspaceId. A {@code null} result
|
||||
* makes the skill lookup scope to builtin/global only — never another
|
||||
* workspace's skill.
|
||||
*/
|
||||
private Long resolveWorkspaceId(ChatOrigin origin) {
|
||||
if (origin == null) return null;
|
||||
if (origin.workspaceId() != null) return origin.workspaceId();
|
||||
return workspaceIdForConversation(origin.conversationId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a conversation's owning workspace via the lookup cache, or
|
||||
* {@code null} when unavailable. Exposed so sibling graph nodes (e.g.
|
||||
* {@code ActionNode}) that only hold a conversationId can scope skill
|
||||
* resolution to the right workspace without their own cache dependency.
|
||||
*/
|
||||
public Long workspaceIdForConversation(String conversationId) {
|
||||
return (workspaceLookupCache != null && conversationId != null)
|
||||
? workspaceLookupCache.resolveByConversation(conversationId) : null;
|
||||
}
|
||||
|
||||
private String skillAwareNotFoundMessage(String toolName, ChatOrigin origin) {
|
||||
if (skillRuntimeService != null && toolName != null && !toolName.isBlank()) {
|
||||
try {
|
||||
boolean isSkill = skillRuntimeService.getActiveSkills().stream()
|
||||
boolean isSkill = skillRuntimeService.getActiveSkills(resolveWorkspaceId(origin)).stream()
|
||||
.anyMatch(s -> s.getName() != null && s.getName().equalsIgnoreCase(toolName));
|
||||
if (isSkill) {
|
||||
return String.format(
|
||||
@ -1398,7 +1422,9 @@ public class ToolExecutionExecutor {
|
||||
private SkillRedirect tryAutoRedirectSkillCall(String toolName, String originalArgs, ChatOrigin origin) {
|
||||
if (skillRuntimeService == null || toolName == null || toolName.isBlank()) return null;
|
||||
try {
|
||||
boolean isSkill = skillRuntimeService.getActiveSkills().stream()
|
||||
// Scope to the conversation's workspace so an agent is never redirected
|
||||
// into (and handed the SKILL.md content of) another workspace's skill.
|
||||
boolean isSkill = skillRuntimeService.getActiveSkills(resolveWorkspaceId(origin)).stream()
|
||||
.anyMatch(s -> s.getName() != null && s.getName().equalsIgnoreCase(toolName));
|
||||
if (!isSkill) return null;
|
||||
} catch (Exception e) {
|
||||
|
||||
@ -213,7 +213,8 @@ public class ActionNode implements NodeAction {
|
||||
}
|
||||
for (String skillName : skillNames) {
|
||||
try {
|
||||
vip.mate.skill.runtime.model.ResolvedSkill skill = skillRuntimeService.findActiveSkill(skillName);
|
||||
vip.mate.skill.runtime.model.ResolvedSkill skill = skillRuntimeService.findActiveSkill(
|
||||
skillName, executor.workspaceIdForConversation(conversationId));
|
||||
if (skill == null || skill.getManifest() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
import vip.mate.agent.context.AgentWorkspaceResolver;
|
||||
import vip.mate.agent.context.ChatOrigin;
|
||||
import vip.mate.llm.routing.AgentBindingResolver;
|
||||
import vip.mate.skill.runtime.SkillRuntimeService;
|
||||
@ -57,6 +58,7 @@ import java.util.Set;
|
||||
public class CodeExecuteTool {
|
||||
|
||||
private final SkillRuntimeService runtimeService;
|
||||
private final AgentWorkspaceResolver workspaceResolver;
|
||||
private final SkillScriptExecutionService executionService;
|
||||
private final SkillSecretService skillSecretService;
|
||||
private final ObjectMapper objectMapper;
|
||||
@ -119,8 +121,9 @@ public class CodeExecuteTool {
|
||||
Map<String, String> envVars = Collections.emptyMap();
|
||||
|
||||
if (skillName != null && !skillName.isBlank()) {
|
||||
// Skill-scoped run: validate binding + resolve the skill directory.
|
||||
ResolvedSkill skill = runtimeService.findActiveSkill(skillName);
|
||||
// Skill-scoped run: validate binding + resolve the skill directory,
|
||||
// scoped to the conversation's workspace (+ builtin/global).
|
||||
ResolvedSkill skill = runtimeService.findActiveSkill(skillName, workspaceResolver.resolve(ChatOrigin.from(ctx)));
|
||||
if (skill == null) {
|
||||
return formatError("Skill '" + skillName + "' not found or not enabled");
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
import vip.mate.agent.context.AgentWorkspaceResolver;
|
||||
import vip.mate.agent.context.ChatOrigin;
|
||||
import vip.mate.agent.context.TokenEstimator;
|
||||
import vip.mate.llm.routing.AgentBindingResolver;
|
||||
@ -42,6 +43,7 @@ public class SkillFileTool {
|
||||
private final SkillRuntimeService runtimeService;
|
||||
private final SkillFileAccessPolicy accessPolicy;
|
||||
private final SkillUsageService usageService;
|
||||
private final AgentWorkspaceResolver workspaceResolver;
|
||||
|
||||
@Lazy
|
||||
@Autowired
|
||||
@ -84,7 +86,7 @@ public class SkillFileTool {
|
||||
log.info("Reading skill file: skill={}, path={}", skillName, filePath);
|
||||
|
||||
// 查找 active skill
|
||||
ResolvedSkill skill = runtimeService.findActiveSkill(skillName);
|
||||
ResolvedSkill skill = runtimeService.findActiveSkill(skillName, workspaceResolver.resolve(ChatOrigin.from(ctx)));
|
||||
if (skill == null) {
|
||||
return "Error: Skill '" + skillName + "' not found or not enabled";
|
||||
}
|
||||
@ -245,7 +247,7 @@ public class SkillFileTool {
|
||||
) {
|
||||
log.info("Listing skill files: skill={}", skillName);
|
||||
|
||||
ResolvedSkill skill = runtimeService.findActiveSkill(skillName);
|
||||
ResolvedSkill skill = runtimeService.findActiveSkill(skillName, workspaceResolver.resolve(ChatOrigin.from(ctx)));
|
||||
if (skill == null) {
|
||||
return "Error: Skill '" + skillName + "' not found or not enabled";
|
||||
}
|
||||
@ -341,7 +343,7 @@ public class SkillFileTool {
|
||||
// entries — no need to thread the (package-private) recommended
|
||||
// comparator back through here.
|
||||
List<ResolvedSkill> activeSkills = SkillCatalogSorter.sortResolved(
|
||||
runtimeService.getActiveSkills().stream()
|
||||
runtimeService.getActiveSkills(workspaceResolver.resolve(ChatOrigin.from(ctx))).stream()
|
||||
.filter(s -> SkillCatalogSorter.sourceMatches(s, source))
|
||||
.filter(s -> SkillCatalogSorter.runtimeMatches(s, status))
|
||||
.filter(s -> boundSkillIds == null
|
||||
|
||||
@ -9,6 +9,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
import vip.mate.agent.context.AgentWorkspaceResolver;
|
||||
import vip.mate.agent.context.ChatOrigin;
|
||||
import vip.mate.llm.routing.AgentBindingResolver;
|
||||
import vip.mate.skill.runtime.SkillRuntimeService;
|
||||
@ -37,6 +38,7 @@ public class SkillLoadTool {
|
||||
|
||||
private final SkillRuntimeService runtimeService;
|
||||
private final SkillFileTool skillFileTool;
|
||||
private final AgentWorkspaceResolver workspaceResolver;
|
||||
|
||||
@Lazy
|
||||
@Autowired
|
||||
@ -69,13 +71,16 @@ public class SkillLoadTool {
|
||||
if (skillName == null || skillName.isBlank()) {
|
||||
return "Error: skillName is required. Call listAvailableSkills() to see loadable skills.";
|
||||
}
|
||||
ResolvedSkill skill = runtimeService.findActiveSkill(skillName);
|
||||
ChatOrigin origin = ChatOrigin.from(ctx);
|
||||
// Resolve only within the conversation's workspace (+ builtin/global), so
|
||||
// an agent can never load another workspace's same-named skill.
|
||||
ResolvedSkill skill = runtimeService.findActiveSkill(skillName, workspaceResolver.resolve(origin));
|
||||
if (skill == null) {
|
||||
log.info("load_skill: skill '{}' not found or not enabled", skillName);
|
||||
return "Error: Skill '" + skillName + "' not found or not enabled. "
|
||||
+ "Call listAvailableSkills(keyword=\"" + skillName + "\") to find the correct name.";
|
||||
}
|
||||
Long agentId = ChatOrigin.from(ctx).agentId();
|
||||
Long agentId = origin.agentId();
|
||||
if (agentId != null) {
|
||||
Set<Long> boundSkillIds = agentBindingResolver.getBoundSkillIds(agentId);
|
||||
if (boundSkillIds != null && (skill.getId() == null || !boundSkillIds.contains(skill.getId()))) {
|
||||
|
||||
@ -13,6 +13,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
import vip.mate.agent.context.AgentWorkspaceResolver;
|
||||
import vip.mate.agent.context.ChatOrigin;
|
||||
import vip.mate.llm.routing.AgentBindingResolver;
|
||||
import vip.mate.skill.runtime.SkillFileAccessPolicy;
|
||||
@ -38,6 +39,7 @@ import java.util.Set;
|
||||
public class SkillScriptTool {
|
||||
|
||||
private final SkillRuntimeService runtimeService;
|
||||
private final AgentWorkspaceResolver workspaceResolver;
|
||||
private final SkillFileAccessPolicy accessPolicy;
|
||||
private final SkillScriptExecutionService executionService;
|
||||
private final SkillSecretService skillSecretService;
|
||||
@ -84,8 +86,8 @@ public class SkillScriptTool {
|
||||
) {
|
||||
log.info("Executing skill script: skill={}, script={}, args={}", skillName, scriptPath, args);
|
||||
|
||||
// Look up active skill.
|
||||
ResolvedSkill skill = runtimeService.findActiveSkill(skillName);
|
||||
// Look up active skill within the conversation's workspace (+ builtin/global).
|
||||
ResolvedSkill skill = runtimeService.findActiveSkill(skillName, workspaceResolver.resolve(ChatOrigin.from(ctx)));
|
||||
if (skill == null) {
|
||||
return formatError("Skill '" + skillName + "' not found or not enabled");
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
@ -52,7 +53,7 @@ class ToolExecutionExecutorSkillAutoRedirectTest {
|
||||
when(s.getName()).thenReturn(name);
|
||||
return s;
|
||||
}).toList();
|
||||
when(svc.getActiveSkills()).thenReturn(skills);
|
||||
when(svc.getActiveSkills(any())).thenReturn(skills);
|
||||
return svc;
|
||||
}
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ import vip.mate.tool.guard.ToolGuardResult;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
@ -36,7 +37,7 @@ class ToolExecutionExecutorSkillHintTest {
|
||||
when(s.getName()).thenReturn(name);
|
||||
return s;
|
||||
}).toList();
|
||||
when(svc.getActiveSkills()).thenReturn(skills);
|
||||
when(svc.getActiveSkills(any())).thenReturn(skills);
|
||||
return svc;
|
||||
}
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ class CodeExecuteToolArgsTest {
|
||||
|
||||
/** Unused collaborators are null — {@code normalizeArgs} only needs the mapper. */
|
||||
private final CodeExecuteTool tool =
|
||||
new CodeExecuteTool(null, null, null, objectMapper, null);
|
||||
new CodeExecuteTool(null, null, null, null, objectMapper, null);
|
||||
|
||||
@Test
|
||||
@DisplayName("null / blank / empty-array args yield no argument list")
|
||||
|
||||
@ -25,7 +25,7 @@ class CodeExecuteToolArtifactTest {
|
||||
@Test
|
||||
@DisplayName("formatResult embeds links so extraction yields the clean filename, not '\"[name'")
|
||||
void formatResultExtractsCleanFilename() {
|
||||
CodeExecuteTool tool = new CodeExecuteTool(null, null, null, null, null);
|
||||
CodeExecuteTool tool = new CodeExecuteTool(null, null, null, null, null, null);
|
||||
var result = vip.mate.skill.runtime.SkillScriptExecutionService.ScriptResult.error(0, "");
|
||||
String out = tool.formatResult(result, List.of(
|
||||
"[report.csv](http://localhost:18088/api/v1/files/generated/abc-123)",
|
||||
@ -43,7 +43,7 @@ class CodeExecuteToolArtifactTest {
|
||||
@Test
|
||||
@DisplayName("No artifacts → no generatedFiles field")
|
||||
void noArtifactsNoField() {
|
||||
CodeExecuteTool tool = new CodeExecuteTool(null, null, null, null, null);
|
||||
CodeExecuteTool tool = new CodeExecuteTool(null, null, null, null, null, null);
|
||||
var result = vip.mate.skill.runtime.SkillScriptExecutionService.ScriptResult.error(0, "ok");
|
||||
String out = tool.formatResult(result, List.of());
|
||||
assertEquals(false, out.contains("generatedFiles"), out);
|
||||
|
||||
@ -4,6 +4,7 @@ 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.AgentWorkspaceResolver;
|
||||
import vip.mate.agent.context.ChatOrigin;
|
||||
import vip.mate.llm.routing.AgentBindingResolver;
|
||||
import vip.mate.skill.runtime.SkillFileAccessPolicy;
|
||||
@ -17,6 +18,8 @@ import java.util.Set;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@ -29,8 +32,10 @@ class SkillFileToolTest {
|
||||
SkillRuntimeService runtimeService = mock(SkillRuntimeService.class);
|
||||
SkillFileAccessPolicy accessPolicy = mock(SkillFileAccessPolicy.class);
|
||||
SkillUsageService usageService = mock(SkillUsageService.class);
|
||||
SkillFileTool tool = new SkillFileTool(runtimeService, accessPolicy, usageService);
|
||||
when(runtimeService.getActiveSkills()).thenReturn(List.of(
|
||||
AgentWorkspaceResolver workspaceResolver = mock(AgentWorkspaceResolver.class);
|
||||
when(workspaceResolver.resolve(any())).thenReturn(1L);
|
||||
SkillFileTool tool = new SkillFileTool(runtimeService, accessPolicy, usageService, workspaceResolver);
|
||||
when(runtimeService.getActiveSkills(any())).thenReturn(List.of(
|
||||
skill("apple-notes", "database", true),
|
||||
skill("ckjia-shopping", "mcp", false),
|
||||
skill("claude-code", "acp", false)));
|
||||
@ -48,10 +53,12 @@ class SkillFileToolTest {
|
||||
SkillRuntimeService runtimeService = mock(SkillRuntimeService.class);
|
||||
SkillFileAccessPolicy accessPolicy = mock(SkillFileAccessPolicy.class);
|
||||
SkillUsageService usageService = mock(SkillUsageService.class);
|
||||
SkillFileTool tool = new SkillFileTool(runtimeService, accessPolicy, usageService);
|
||||
AgentWorkspaceResolver workspaceResolver = mock(AgentWorkspaceResolver.class);
|
||||
when(workspaceResolver.resolve(any())).thenReturn(1L);
|
||||
SkillFileTool tool = new SkillFileTool(runtimeService, accessPolicy, usageService, workspaceResolver);
|
||||
ResolvedSkill skill = skill("browser-cdp", "database", true);
|
||||
skill.setContent("# Browser CDP\nUse devtools.");
|
||||
when(runtimeService.findActiveSkill("browser-cdp")).thenReturn(skill);
|
||||
when(runtimeService.findActiveSkill(eq("browser-cdp"), any())).thenReturn(skill);
|
||||
|
||||
String content = tool.readSkillFile("browser-cdp", "SKILL.md", null, null, null);
|
||||
|
||||
@ -70,10 +77,12 @@ class SkillFileToolTest {
|
||||
SkillRuntimeService runtimeService = mock(SkillRuntimeService.class);
|
||||
SkillFileAccessPolicy accessPolicy = mock(SkillFileAccessPolicy.class);
|
||||
SkillUsageService usageService = mock(SkillUsageService.class);
|
||||
SkillFileTool tool = new SkillFileTool(runtimeService, accessPolicy, usageService);
|
||||
AgentWorkspaceResolver workspaceResolver = mock(AgentWorkspaceResolver.class);
|
||||
when(workspaceResolver.resolve(any())).thenReturn(1L);
|
||||
SkillFileTool tool = new SkillFileTool(runtimeService, accessPolicy, usageService, workspaceResolver);
|
||||
ResolvedSkill skill = skill("large-skill", "database", true);
|
||||
skill.setContent("line\n".repeat(500));
|
||||
when(runtimeService.findActiveSkill("large-skill")).thenReturn(skill);
|
||||
when(runtimeService.findActiveSkill(eq("large-skill"), any())).thenReturn(skill);
|
||||
|
||||
String content = tool.readSkillFile("large-skill", "SKILL.md", 10, 20, null);
|
||||
|
||||
@ -94,12 +103,14 @@ class SkillFileToolTest {
|
||||
SkillRuntimeService runtimeService = mock(SkillRuntimeService.class);
|
||||
SkillFileAccessPolicy accessPolicy = mock(SkillFileAccessPolicy.class);
|
||||
SkillUsageService usageService = mock(SkillUsageService.class);
|
||||
SkillFileTool tool = new SkillFileTool(runtimeService, accessPolicy, usageService);
|
||||
AgentWorkspaceResolver workspaceResolver = mock(AgentWorkspaceResolver.class);
|
||||
when(workspaceResolver.resolve(any())).thenReturn(1L);
|
||||
SkillFileTool tool = new SkillFileTool(runtimeService, accessPolicy, usageService, workspaceResolver);
|
||||
ResolvedSkill skill = skill("huge-line-skill", "database", true);
|
||||
// 12 KB single line — well past MAX_OUTPUT_CHARS (8KB).
|
||||
String hugeLine = "x".repeat(12_000);
|
||||
skill.setContent(hugeLine + "\nsecond line\nthird line\n");
|
||||
when(runtimeService.findActiveSkill("huge-line-skill")).thenReturn(skill);
|
||||
when(runtimeService.findActiveSkill(eq("huge-line-skill"), any())).thenReturn(skill);
|
||||
|
||||
String content = tool.readSkillFile("huge-line-skill", "SKILL.md", 1, 5, null);
|
||||
|
||||
@ -126,12 +137,14 @@ class SkillFileToolTest {
|
||||
SkillRuntimeService runtimeService = mock(SkillRuntimeService.class);
|
||||
SkillFileAccessPolicy accessPolicy = mock(SkillFileAccessPolicy.class);
|
||||
SkillUsageService usageService = mock(SkillUsageService.class);
|
||||
SkillFileTool tool = new SkillFileTool(runtimeService, accessPolicy, usageService);
|
||||
AgentWorkspaceResolver workspaceResolver = mock(AgentWorkspaceResolver.class);
|
||||
when(workspaceResolver.resolve(any())).thenReturn(1L);
|
||||
SkillFileTool tool = new SkillFileTool(runtimeService, accessPolicy, usageService, workspaceResolver);
|
||||
ResolvedSkill skill = skill("large-skill", "database", true);
|
||||
// 500 lines * 5 chars = 2500 chars; 250 lines is also above DEFAULT_MAX_LINES (200).
|
||||
String body = "line\n".repeat(500);
|
||||
skill.setContent(body);
|
||||
when(runtimeService.findActiveSkill("large-skill")).thenReturn(skill);
|
||||
when(runtimeService.findActiveSkill(eq("large-skill"), any())).thenReturn(skill);
|
||||
|
||||
String content = tool.readSkillFile("large-skill", "SKILL.md", null, null, null);
|
||||
|
||||
@ -148,12 +161,14 @@ class SkillFileToolTest {
|
||||
SkillFileAccessPolicy accessPolicy = mock(SkillFileAccessPolicy.class);
|
||||
SkillUsageService usageService = mock(SkillUsageService.class);
|
||||
AgentBindingResolver bindingResolver = mock(AgentBindingResolver.class);
|
||||
SkillFileTool tool = new SkillFileTool(runtimeService, accessPolicy, usageService);
|
||||
AgentWorkspaceResolver workspaceResolver = mock(AgentWorkspaceResolver.class);
|
||||
when(workspaceResolver.resolve(any())).thenReturn(1L);
|
||||
SkillFileTool tool = new SkillFileTool(runtimeService, accessPolicy, usageService, workspaceResolver);
|
||||
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));
|
||||
when(runtimeService.getActiveSkills(any())).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()));
|
||||
|
||||
@ -171,12 +186,14 @@ class SkillFileToolTest {
|
||||
SkillFileAccessPolicy accessPolicy = mock(SkillFileAccessPolicy.class);
|
||||
SkillUsageService usageService = mock(SkillUsageService.class);
|
||||
AgentBindingResolver bindingResolver = mock(AgentBindingResolver.class);
|
||||
SkillFileTool tool = new SkillFileTool(runtimeService, accessPolicy, usageService);
|
||||
AgentWorkspaceResolver workspaceResolver = mock(AgentWorkspaceResolver.class);
|
||||
when(workspaceResolver.resolve(any())).thenReturn(1L);
|
||||
SkillFileTool tool = new SkillFileTool(runtimeService, accessPolicy, usageService, workspaceResolver);
|
||||
ReflectionTestUtils.setField(tool, "agentBindingResolver", bindingResolver);
|
||||
|
||||
ResolvedSkill beta = skill("beta-skill", "database", true);
|
||||
beta.setContent("# Beta\nsecret body");
|
||||
when(runtimeService.findActiveSkill("beta-skill")).thenReturn(beta);
|
||||
when(runtimeService.findActiveSkill(eq("beta-skill"), any())).thenReturn(beta);
|
||||
// Bound to some other skill id, never beta's.
|
||||
when(bindingResolver.getBoundSkillIds(42L)).thenReturn(Set.of(999L));
|
||||
|
||||
@ -194,11 +211,13 @@ class SkillFileToolTest {
|
||||
SkillFileAccessPolicy accessPolicy = mock(SkillFileAccessPolicy.class);
|
||||
SkillUsageService usageService = mock(SkillUsageService.class);
|
||||
AgentBindingResolver bindingResolver = mock(AgentBindingResolver.class);
|
||||
SkillFileTool tool = new SkillFileTool(runtimeService, accessPolicy, usageService);
|
||||
AgentWorkspaceResolver workspaceResolver = mock(AgentWorkspaceResolver.class);
|
||||
when(workspaceResolver.resolve(any())).thenReturn(1L);
|
||||
SkillFileTool tool = new SkillFileTool(runtimeService, accessPolicy, usageService, workspaceResolver);
|
||||
ReflectionTestUtils.setField(tool, "agentBindingResolver", bindingResolver);
|
||||
|
||||
ResolvedSkill beta = skill("beta-skill", "database", true);
|
||||
when(runtimeService.findActiveSkill("beta-skill")).thenReturn(beta);
|
||||
when(runtimeService.findActiveSkill(eq("beta-skill"), any())).thenReturn(beta);
|
||||
when(bindingResolver.getBoundSkillIds(42L)).thenReturn(Set.of(999L));
|
||||
|
||||
ToolContext ctx = ChatOrigin.EMPTY.withAgent(42L).toToolContext();
|
||||
@ -214,12 +233,14 @@ class SkillFileToolTest {
|
||||
SkillFileAccessPolicy accessPolicy = mock(SkillFileAccessPolicy.class);
|
||||
SkillUsageService usageService = mock(SkillUsageService.class);
|
||||
AgentBindingResolver bindingResolver = mock(AgentBindingResolver.class);
|
||||
SkillFileTool tool = new SkillFileTool(runtimeService, accessPolicy, usageService);
|
||||
AgentWorkspaceResolver workspaceResolver = mock(AgentWorkspaceResolver.class);
|
||||
when(workspaceResolver.resolve(any())).thenReturn(1L);
|
||||
SkillFileTool tool = new SkillFileTool(runtimeService, accessPolicy, usageService, workspaceResolver);
|
||||
ReflectionTestUtils.setField(tool, "agentBindingResolver", bindingResolver);
|
||||
|
||||
ResolvedSkill beta = skill("beta-skill", "database", true);
|
||||
beta.setContent("# Beta\nvisible body");
|
||||
when(runtimeService.findActiveSkill("beta-skill")).thenReturn(beta);
|
||||
when(runtimeService.findActiveSkill(eq("beta-skill"), any())).thenReturn(beta);
|
||||
// null == no explicit binding restriction → inherit every enabled skill.
|
||||
when(bindingResolver.getBoundSkillIds(42L)).thenReturn(null);
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ package vip.mate.tool.builtin;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import vip.mate.agent.context.AgentWorkspaceResolver;
|
||||
import vip.mate.skill.runtime.SkillRuntimeService;
|
||||
import vip.mate.skill.runtime.model.ResolvedSkill;
|
||||
|
||||
@ -26,7 +27,8 @@ class SkillLoadToolTest {
|
||||
void blankSkillNameRejected() {
|
||||
SkillRuntimeService runtime = mock(SkillRuntimeService.class);
|
||||
SkillFileTool fileTool = mock(SkillFileTool.class);
|
||||
SkillLoadTool tool = new SkillLoadTool(runtime, fileTool);
|
||||
AgentWorkspaceResolver workspaceResolver = mock(AgentWorkspaceResolver.class);
|
||||
SkillLoadTool tool = new SkillLoadTool(runtime, fileTool, workspaceResolver);
|
||||
|
||||
String out = tool.loadSkill(" ", null, null);
|
||||
|
||||
@ -40,8 +42,10 @@ class SkillLoadToolTest {
|
||||
void unknownSkillReturnsError() {
|
||||
SkillRuntimeService runtime = mock(SkillRuntimeService.class);
|
||||
SkillFileTool fileTool = mock(SkillFileTool.class);
|
||||
when(runtime.findActiveSkill("nope")).thenReturn(null);
|
||||
SkillLoadTool tool = new SkillLoadTool(runtime, fileTool);
|
||||
AgentWorkspaceResolver workspaceResolver = mock(AgentWorkspaceResolver.class);
|
||||
when(workspaceResolver.resolve(any())).thenReturn(1L);
|
||||
when(runtime.findActiveSkill(eq("nope"), any())).thenReturn(null);
|
||||
SkillLoadTool tool = new SkillLoadTool(runtime, fileTool, workspaceResolver);
|
||||
|
||||
String out = tool.loadSkill("nope", null, null);
|
||||
|
||||
@ -55,10 +59,12 @@ class SkillLoadToolTest {
|
||||
void loadsSkillMdByDefault() {
|
||||
SkillRuntimeService runtime = mock(SkillRuntimeService.class);
|
||||
SkillFileTool fileTool = mock(SkillFileTool.class);
|
||||
when(runtime.findActiveSkill("foo")).thenReturn(skill("foo"));
|
||||
AgentWorkspaceResolver workspaceResolver = mock(AgentWorkspaceResolver.class);
|
||||
when(workspaceResolver.resolve(any())).thenReturn(1L);
|
||||
when(runtime.findActiveSkill(eq("foo"), any())).thenReturn(skill("foo"));
|
||||
when(fileTool.readSkillFile(eq("foo"), eq("SKILL.md"), isNull(), isNull(), any()))
|
||||
.thenReturn("SKILL CONTENT");
|
||||
SkillLoadTool tool = new SkillLoadTool(runtime, fileTool);
|
||||
SkillLoadTool tool = new SkillLoadTool(runtime, fileTool, workspaceResolver);
|
||||
|
||||
String out = tool.loadSkill("foo", null, null);
|
||||
|
||||
@ -71,10 +77,12 @@ class SkillLoadToolTest {
|
||||
void loadsExplicitSubFile() {
|
||||
SkillRuntimeService runtime = mock(SkillRuntimeService.class);
|
||||
SkillFileTool fileTool = mock(SkillFileTool.class);
|
||||
when(runtime.findActiveSkill("foo")).thenReturn(skill("foo"));
|
||||
AgentWorkspaceResolver workspaceResolver = mock(AgentWorkspaceResolver.class);
|
||||
when(workspaceResolver.resolve(any())).thenReturn(1L);
|
||||
when(runtime.findActiveSkill(eq("foo"), any())).thenReturn(skill("foo"));
|
||||
when(fileTool.readSkillFile(eq("foo"), eq("references/api.md"), isNull(), isNull(), any()))
|
||||
.thenReturn("REF CONTENT");
|
||||
SkillLoadTool tool = new SkillLoadTool(runtime, fileTool);
|
||||
SkillLoadTool tool = new SkillLoadTool(runtime, fileTool, workspaceResolver);
|
||||
|
||||
String out = tool.loadSkill("foo", "references/api.md", null);
|
||||
|
||||
|
||||
@ -26,7 +26,7 @@ class SkillScriptToolArgsTest {
|
||||
|
||||
/** Unused collaborators are null — {@code normalizeArgs} only needs the mapper. */
|
||||
private final SkillScriptTool tool =
|
||||
new SkillScriptTool(null, null, null, null, objectMapper);
|
||||
new SkillScriptTool(null, null, null, null, null, objectMapper);
|
||||
|
||||
@Test
|
||||
@DisplayName("null / blank args yield no argument list")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user