mirror of
https://github.com/slhaf/Partner.git
synced 2026-05-12 08:43:02 +08:00
refactor(runner): adjust the directory organization of runner
This commit is contained in:
@@ -9,6 +9,12 @@ import work.slhaf.partner.core.action.entity.ActionFileMetaData;
|
|||||||
import work.slhaf.partner.core.action.entity.MetaAction;
|
import work.slhaf.partner.core.action.entity.MetaAction;
|
||||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||||
import work.slhaf.partner.core.action.exception.ActionInitFailedException;
|
import work.slhaf.partner.core.action.exception.ActionInitFailedException;
|
||||||
|
import work.slhaf.partner.core.action.runner.execution.CommandExecutionService;
|
||||||
|
import work.slhaf.partner.core.action.runner.execution.LocalProcessCommandExecutionService;
|
||||||
|
import work.slhaf.partner.core.action.runner.execution.McpActionExecutor;
|
||||||
|
import work.slhaf.partner.core.action.runner.execution.OriginExecutionService;
|
||||||
|
import work.slhaf.partner.core.action.runner.mcp.*;
|
||||||
|
import work.slhaf.partner.core.action.runner.support.ActionSerializer;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -22,8 +28,8 @@ import static work.slhaf.partner.common.util.PathUtil.buildPathStr;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class LocalRunnerClient extends RunnerClient implements AutoCloseable {
|
public class LocalRunnerClient extends RunnerClient implements AutoCloseable {
|
||||||
|
|
||||||
static final String MCP_NAME_DESC = "mcp-desc";
|
public static final String MCP_NAME_DESC = "mcp-desc";
|
||||||
static final String MCP_NAME_DYNAMIC = "mcp-dynamic";
|
public static final String MCP_NAME_DYNAMIC = "mcp-dynamic";
|
||||||
|
|
||||||
private final String tmpActionPath;
|
private final String tmpActionPath;
|
||||||
private final String dynamicActionPath;
|
private final String dynamicActionPath;
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
package work.slhaf.partner.core.action.runner;
|
|
||||||
|
|
||||||
interface RunnerExecutionPolicy {
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package work.slhaf.partner.core.action.runner;
|
package work.slhaf.partner.core.action.runner.execution;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
interface CommandExecutionService {
|
public interface CommandExecutionService {
|
||||||
|
|
||||||
String[] buildCommands(String ext, Map<String, Object> params, String absolutePath);
|
String[] buildCommands(String ext, Map<String, Object> params, String absolutePath);
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package work.slhaf.partner.core.action.runner;
|
package work.slhaf.partner.core.action.runner.execution;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
@@ -7,7 +7,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
class LocalProcessCommandExecutionService implements CommandExecutionService {
|
public class LocalProcessCommandExecutionService implements CommandExecutionService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String[] buildCommands(String ext, Map<String, Object> params, String absolutePath) {
|
public String[] buildCommands(String ext, Map<String, Object> params, String absolutePath) {
|
||||||
@@ -1,21 +1,23 @@
|
|||||||
package work.slhaf.partner.core.action.runner;
|
package work.slhaf.partner.core.action.runner.execution;
|
||||||
|
|
||||||
import io.modelcontextprotocol.client.McpSyncClient;
|
import io.modelcontextprotocol.client.McpSyncClient;
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
import work.slhaf.partner.core.action.entity.MetaAction;
|
import work.slhaf.partner.core.action.entity.MetaAction;
|
||||||
|
import work.slhaf.partner.core.action.runner.RunnerClient;
|
||||||
|
import work.slhaf.partner.core.action.runner.mcp.McpClientRegistry;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
class McpActionExecutor {
|
public class McpActionExecutor {
|
||||||
|
|
||||||
private final McpClientRegistry mcpClientRegistry;
|
private final McpClientRegistry mcpClientRegistry;
|
||||||
|
|
||||||
McpActionExecutor(McpClientRegistry mcpClientRegistry) {
|
public McpActionExecutor(McpClientRegistry mcpClientRegistry) {
|
||||||
this.mcpClientRegistry = mcpClientRegistry;
|
this.mcpClientRegistry = mcpClientRegistry;
|
||||||
}
|
}
|
||||||
|
|
||||||
RunnerClient.RunnerResponse run(MetaAction metaAction) {
|
public RunnerClient.RunnerResponse run(MetaAction metaAction) {
|
||||||
RunnerClient.RunnerResponse response = new RunnerClient.RunnerResponse();
|
RunnerClient.RunnerResponse response = new RunnerClient.RunnerResponse();
|
||||||
McpSyncClient mcpClient = mcpClientRegistry.get(metaAction.getLocation());
|
McpSyncClient mcpClient = mcpClientRegistry.get(metaAction.getLocation());
|
||||||
if (mcpClient == null) {
|
if (mcpClient == null) {
|
||||||
@@ -1,19 +1,20 @@
|
|||||||
package work.slhaf.partner.core.action.runner;
|
package work.slhaf.partner.core.action.runner.execution;
|
||||||
|
|
||||||
import cn.hutool.core.io.FileUtil;
|
import cn.hutool.core.io.FileUtil;
|
||||||
import work.slhaf.partner.core.action.entity.MetaAction;
|
import work.slhaf.partner.core.action.entity.MetaAction;
|
||||||
|
import work.slhaf.partner.core.action.runner.RunnerClient;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
class OriginExecutionService {
|
public class OriginExecutionService {
|
||||||
|
|
||||||
private final CommandExecutionService commandExecutionService;
|
private final CommandExecutionService commandExecutionService;
|
||||||
|
|
||||||
OriginExecutionService(CommandExecutionService commandExecutionService) {
|
public OriginExecutionService(CommandExecutionService commandExecutionService) {
|
||||||
this.commandExecutionService = commandExecutionService;
|
this.commandExecutionService = commandExecutionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
RunnerClient.RunnerResponse run(MetaAction metaAction) {
|
public RunnerClient.RunnerResponse run(MetaAction metaAction) {
|
||||||
RunnerClient.RunnerResponse response = new RunnerClient.RunnerResponse();
|
RunnerClient.RunnerResponse response = new RunnerClient.RunnerResponse();
|
||||||
File file = new File(metaAction.getLocation());
|
File file = new File(metaAction.getLocation());
|
||||||
String ext = FileUtil.getSuffix(file);
|
String ext = FileUtil.getSuffix(file);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package work.slhaf.partner.core.action.runner;
|
package work.slhaf.partner.core.action.runner.mcp;
|
||||||
|
|
||||||
import cn.hutool.core.io.FileUtil;
|
import cn.hutool.core.io.FileUtil;
|
||||||
import cn.hutool.json.JSONUtil;
|
import cn.hutool.json.JSONUtil;
|
||||||
@@ -15,6 +15,8 @@ import reactor.core.scheduler.Schedulers;
|
|||||||
import work.slhaf.partner.common.mcp.InProcessMcpTransport;
|
import work.slhaf.partner.common.mcp.InProcessMcpTransport;
|
||||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||||
import work.slhaf.partner.core.action.exception.ActionInitFailedException;
|
import work.slhaf.partner.core.action.exception.ActionInitFailedException;
|
||||||
|
import work.slhaf.partner.core.action.runner.execution.CommandExecutionService;
|
||||||
|
import work.slhaf.partner.core.action.runner.support.DirectoryWatchSupport;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -33,7 +35,7 @@ import java.util.stream.Collectors;
|
|||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
class DynamicActionMcpManager implements AutoCloseable {
|
public class DynamicActionMcpManager implements AutoCloseable {
|
||||||
|
|
||||||
private final Path root;
|
private final Path root;
|
||||||
private final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions;
|
private final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions;
|
||||||
@@ -42,7 +44,7 @@ class DynamicActionMcpManager implements AutoCloseable {
|
|||||||
private final InProcessMcpTransport clientTransport;
|
private final InProcessMcpTransport clientTransport;
|
||||||
private final DirectoryWatchSupport watchSupport;
|
private final DirectoryWatchSupport watchSupport;
|
||||||
|
|
||||||
DynamicActionMcpManager(Path root,
|
public DynamicActionMcpManager(Path root,
|
||||||
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions,
|
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions,
|
||||||
ExecutorService executor,
|
ExecutorService executor,
|
||||||
CommandExecutionService commandExecutionService) throws IOException {
|
CommandExecutionService commandExecutionService) throws IOException {
|
||||||
@@ -65,11 +67,11 @@ class DynamicActionMcpManager implements AutoCloseable {
|
|||||||
.onOverflow((thisDir, context) -> reconcile());
|
.onOverflow((thisDir, context) -> reconcile());
|
||||||
}
|
}
|
||||||
|
|
||||||
McpTransportConfig.InProcess clientConfig(int timeout) {
|
public McpTransportConfig.InProcess clientConfig(int timeout) {
|
||||||
return new McpTransportConfig.InProcess(timeout, clientTransport);
|
return new McpTransportConfig.InProcess(timeout, clientTransport);
|
||||||
}
|
}
|
||||||
|
|
||||||
void start() {
|
public void start() {
|
||||||
watchSupport.start();
|
watchSupport.start();
|
||||||
log.info("DynamicActionMcp 文件监听注册完毕");
|
log.info("DynamicActionMcp 文件监听注册完毕");
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package work.slhaf.partner.core.action.runner;
|
package work.slhaf.partner.core.action.runner.mcp;
|
||||||
|
|
||||||
import io.modelcontextprotocol.client.McpSyncClient;
|
import io.modelcontextprotocol.client.McpSyncClient;
|
||||||
|
|
||||||
@@ -6,22 +6,22 @@ import java.util.HashSet;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
class McpClientRegistry implements AutoCloseable {
|
public class McpClientRegistry implements AutoCloseable {
|
||||||
|
|
||||||
private final ConcurrentHashMap<String, McpSyncClient> clients = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, McpSyncClient> clients = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
McpSyncClient get(String serverName) {
|
public McpSyncClient get(String serverName) {
|
||||||
return clients.get(serverName);
|
return clients.get(serverName);
|
||||||
}
|
}
|
||||||
|
|
||||||
void register(String serverName, McpSyncClient client) {
|
public void register(String serverName, McpSyncClient client) {
|
||||||
McpSyncClient old = clients.put(serverName, client);
|
McpSyncClient old = clients.put(serverName, client);
|
||||||
if (old != null && old != client) {
|
if (old != null && old != client) {
|
||||||
old.close();
|
old.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
McpSyncClient remove(String serverName) {
|
public McpSyncClient remove(String serverName) {
|
||||||
McpSyncClient client = detach(serverName);
|
McpSyncClient client = detach(serverName);
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
client.close();
|
client.close();
|
||||||
@@ -29,15 +29,15 @@ class McpClientRegistry implements AutoCloseable {
|
|||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
McpSyncClient detach(String serverName) {
|
public McpSyncClient detach(String serverName) {
|
||||||
return clients.remove(serverName);
|
return clients.remove(serverName);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean contains(String serverName) {
|
public boolean contains(String serverName) {
|
||||||
return clients.containsKey(serverName);
|
return clients.containsKey(serverName);
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<String> listIds() {
|
public Set<String> listIds() {
|
||||||
return new HashSet<>(clients.keySet());
|
return new HashSet<>(clients.keySet());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package work.slhaf.partner.core.action.runner;
|
package work.slhaf.partner.core.action.runner.mcp;
|
||||||
|
|
||||||
import cn.hutool.json.JSONUtil;
|
import cn.hutool.json.JSONUtil;
|
||||||
import io.modelcontextprotocol.client.McpClient;
|
import io.modelcontextprotocol.client.McpClient;
|
||||||
@@ -6,6 +6,8 @@ import io.modelcontextprotocol.client.McpSyncClient;
|
|||||||
import io.modelcontextprotocol.spec.McpSchema;
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||||
|
import work.slhaf.partner.core.action.runner.LocalRunnerClient;
|
||||||
|
import work.slhaf.partner.core.action.runner.support.DirectoryWatchSupport;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -20,7 +22,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
class McpConfigWatcher implements AutoCloseable {
|
public class McpConfigWatcher implements AutoCloseable {
|
||||||
|
|
||||||
private final Path root;
|
private final Path root;
|
||||||
private final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions;
|
private final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions;
|
||||||
@@ -30,7 +32,7 @@ class McpConfigWatcher implements AutoCloseable {
|
|||||||
private final DirectoryWatchSupport watchSupport;
|
private final DirectoryWatchSupport watchSupport;
|
||||||
private final Map<File, McpConfigFileRecord> mcpConfigFileCache = new HashMap<>();
|
private final Map<File, McpConfigFileRecord> mcpConfigFileCache = new HashMap<>();
|
||||||
|
|
||||||
McpConfigWatcher(Path root,
|
public McpConfigWatcher(Path root,
|
||||||
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions,
|
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions,
|
||||||
McpClientRegistry mcpClientRegistry,
|
McpClientRegistry mcpClientRegistry,
|
||||||
McpTransportFactory mcpTransportFactory,
|
McpTransportFactory mcpTransportFactory,
|
||||||
@@ -48,7 +50,7 @@ class McpConfigWatcher implements AutoCloseable {
|
|||||||
.onOverflow((thisDir, context) -> checkAndReload(false));
|
.onOverflow((thisDir, context) -> checkAndReload(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
void start() {
|
public void start() {
|
||||||
watchSupport.start();
|
watchSupport.start();
|
||||||
log.info("CommonMcp 文件监听注册完毕");
|
log.info("CommonMcp 文件监听注册完毕");
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package work.slhaf.partner.core.action.runner;
|
package work.slhaf.partner.core.action.runner.mcp;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import work.slhaf.partner.core.action.runner.support.DirectoryWatchSupport;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -8,13 +9,13 @@ import java.nio.file.Path;
|
|||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
class McpDescWatcher implements AutoCloseable {
|
public class McpDescWatcher implements AutoCloseable {
|
||||||
|
|
||||||
private final Path root;
|
private final Path root;
|
||||||
private final McpMetaRegistry mcpMetaRegistry;
|
private final McpMetaRegistry mcpMetaRegistry;
|
||||||
private final DirectoryWatchSupport watchSupport;
|
private final DirectoryWatchSupport watchSupport;
|
||||||
|
|
||||||
McpDescWatcher(Path root, McpMetaRegistry mcpMetaRegistry, ExecutorService executor) throws IOException {
|
public McpDescWatcher(Path root, McpMetaRegistry mcpMetaRegistry, ExecutorService executor) throws IOException {
|
||||||
this.root = root;
|
this.root = root;
|
||||||
this.mcpMetaRegistry = mcpMetaRegistry;
|
this.mcpMetaRegistry = mcpMetaRegistry;
|
||||||
this.watchSupport = new DirectoryWatchSupport(new DirectoryWatchSupport.Context(root), executor, true, () -> mcpMetaRegistry.loadDirectory(root))
|
this.watchSupport = new DirectoryWatchSupport(new DirectoryWatchSupport.Context(root), executor, true, () -> mcpMetaRegistry.loadDirectory(root))
|
||||||
@@ -24,7 +25,7 @@ class McpDescWatcher implements AutoCloseable {
|
|||||||
.onOverflow((thisDir, context) -> mcpMetaRegistry.reconcile(root));
|
.onOverflow((thisDir, context) -> mcpMetaRegistry.reconcile(root));
|
||||||
}
|
}
|
||||||
|
|
||||||
void start() {
|
public void start() {
|
||||||
watchSupport.start();
|
watchSupport.start();
|
||||||
log.info("DescMcp 文件监听注册完毕");
|
log.info("DescMcp 文件监听注册完毕");
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package work.slhaf.partner.core.action.runner;
|
package work.slhaf.partner.core.action.runner.mcp;
|
||||||
|
|
||||||
import cn.hutool.json.JSONUtil;
|
import cn.hutool.json.JSONUtil;
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
@@ -24,7 +24,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
class McpMetaRegistry implements AutoCloseable {
|
public class McpMetaRegistry implements AutoCloseable {
|
||||||
|
|
||||||
private final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions;
|
private final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions;
|
||||||
private final ConcurrentHashMap<String, String> descCache = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, String> descCache = new ConcurrentHashMap<>();
|
||||||
@@ -33,7 +33,7 @@ class McpMetaRegistry implements AutoCloseable {
|
|||||||
private final McpStatelessAsyncServer descServer;
|
private final McpStatelessAsyncServer descServer;
|
||||||
private final InProcessMcpTransport clientTransport;
|
private final InProcessMcpTransport clientTransport;
|
||||||
|
|
||||||
McpMetaRegistry(ConcurrentHashMap<String, MetaActionInfo> existedMetaActions) {
|
public McpMetaRegistry(ConcurrentHashMap<String, MetaActionInfo> existedMetaActions) {
|
||||||
this.existedMetaActions = existedMetaActions;
|
this.existedMetaActions = existedMetaActions;
|
||||||
InProcessMcpTransport.Pair pair = InProcessMcpTransport.pair();
|
InProcessMcpTransport.Pair pair = InProcessMcpTransport.pair();
|
||||||
this.clientTransport = pair.clientSide();
|
this.clientTransport = pair.clientSide();
|
||||||
@@ -46,11 +46,11 @@ class McpMetaRegistry implements AutoCloseable {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
McpTransportConfig.InProcess clientConfig(String serverName, int timeout) {
|
public McpTransportConfig.InProcess clientConfig(String serverName, int timeout) {
|
||||||
return new McpTransportConfig.InProcess(timeout, clientTransport);
|
return new McpTransportConfig.InProcess(timeout, clientTransport);
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadDirectory(Path root) {
|
public void loadDirectory(Path root) {
|
||||||
File[] files = loadFiles(root);
|
File[] files = loadFiles(root);
|
||||||
if (files == null) {
|
if (files == null) {
|
||||||
return;
|
return;
|
||||||
@@ -60,11 +60,11 @@ class McpMetaRegistry implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean addOrUpdate(Path path) {
|
public boolean addOrUpdate(Path path) {
|
||||||
return addOrUpdate(path.toFile());
|
return addOrUpdate(path.toFile());
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean addOrUpdate(File file) {
|
public boolean addOrUpdate(File file) {
|
||||||
String name = file.getName();
|
String name = file.getName();
|
||||||
if (!isValidDescFile(name)) {
|
if (!isValidDescFile(name)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -86,7 +86,7 @@ class McpMetaRegistry implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void remove(Path path) {
|
public void remove(Path path) {
|
||||||
String uri = path.toUri().toString();
|
String uri = path.toUri().toString();
|
||||||
String actionKey = path.getFileName().toString().replace(".desc.json", "");
|
String actionKey = path.getFileName().toString().replace(".desc.json", "");
|
||||||
descCache.remove(uri);
|
descCache.remove(uri);
|
||||||
@@ -103,7 +103,7 @@ class McpMetaRegistry implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void reconcile(Path root) {
|
public void reconcile(Path root) {
|
||||||
File[] files = loadFiles(root);
|
File[] files = loadFiles(root);
|
||||||
if (files == null) {
|
if (files == null) {
|
||||||
return;
|
return;
|
||||||
@@ -133,7 +133,7 @@ class McpMetaRegistry implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MetaActionInfo buildMetaActionInfo(String serverId, McpSchema.Tool tool) {
|
public MetaActionInfo buildMetaActionInfo(String serverId, McpSchema.Tool tool) {
|
||||||
String actionKey = serverId + "::" + tool.name();
|
String actionKey = serverId + "::" + tool.name();
|
||||||
MetaActionInfo baseInfo = buildToolMetaActionInfo(tool);
|
MetaActionInfo baseInfo = buildToolMetaActionInfo(tool);
|
||||||
originalInfoCache.put(actionKey, copyMetaActionInfo(baseInfo));
|
originalInfoCache.put(actionKey, copyMetaActionInfo(baseInfo));
|
||||||
@@ -161,7 +161,7 @@ class McpMetaRegistry implements AutoCloseable {
|
|||||||
return new McpStatelessServerFeatures.AsyncResourceSpecification(resource, readHandler);
|
return new McpStatelessServerFeatures.AsyncResourceSpecification(resource, readHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isValidDescFile(String fileName) {
|
public boolean isValidDescFile(String fileName) {
|
||||||
return fileName.endsWith(".desc.json") && fileName.contains("::");
|
return fileName.endsWith(".desc.json") && fileName.contains("::");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package work.slhaf.partner.core.action.runner;
|
package work.slhaf.partner.core.action.runner.mcp;
|
||||||
|
|
||||||
import work.slhaf.partner.common.mcp.InProcessMcpTransport;
|
import work.slhaf.partner.common.mcp.InProcessMcpTransport;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
sealed interface McpTransportConfig permits McpTransportConfig.Http, McpTransportConfig.Stdio, McpTransportConfig.InProcess {
|
public sealed interface McpTransportConfig permits McpTransportConfig.Http, McpTransportConfig.Stdio, McpTransportConfig.InProcess {
|
||||||
|
|
||||||
int timeout();
|
int timeout();
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package work.slhaf.partner.core.action.runner;
|
package work.slhaf.partner.core.action.runner.mcp;
|
||||||
|
|
||||||
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
|
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
|
||||||
import io.modelcontextprotocol.client.transport.ServerParameters;
|
import io.modelcontextprotocol.client.transport.ServerParameters;
|
||||||
@@ -7,13 +7,14 @@ import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequ
|
|||||||
import io.modelcontextprotocol.common.McpTransportContext;
|
import io.modelcontextprotocol.common.McpTransportContext;
|
||||||
import io.modelcontextprotocol.json.McpJsonMapper;
|
import io.modelcontextprotocol.json.McpJsonMapper;
|
||||||
import io.modelcontextprotocol.spec.McpClientTransport;
|
import io.modelcontextprotocol.spec.McpClientTransport;
|
||||||
|
import work.slhaf.partner.core.action.runner.policy.RunnerExecutionPolicy;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
|
|
||||||
class McpTransportFactory {
|
public class McpTransportFactory {
|
||||||
|
|
||||||
McpClientTransport create(McpTransportConfig config, RunnerExecutionPolicy policy) {
|
public McpClientTransport create(McpTransportConfig config, RunnerExecutionPolicy policy) {
|
||||||
return switch (config) {
|
return switch (config) {
|
||||||
case McpTransportConfig.Stdio stdio -> {
|
case McpTransportConfig.Stdio stdio -> {
|
||||||
ServerParameters serverParameters = ServerParameters.builder(stdio.command())
|
ServerParameters serverParameters = ServerParameters.builder(stdio.command())
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package work.slhaf.partner.core.action.runner.policy;
|
||||||
|
|
||||||
|
public interface RunnerExecutionPolicy {
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package work.slhaf.partner.core.action.runner;
|
package work.slhaf.partner.core.action.runner.support;
|
||||||
|
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -17,17 +17,17 @@ import java.nio.file.Path;
|
|||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
class ActionSerializer {
|
public class ActionSerializer {
|
||||||
|
|
||||||
private final String tmpActionPath;
|
private final String tmpActionPath;
|
||||||
private final String dynamicActionPath;
|
private final String dynamicActionPath;
|
||||||
|
|
||||||
ActionSerializer(String tmpActionPath, String dynamicActionPath) {
|
public ActionSerializer(String tmpActionPath, String dynamicActionPath) {
|
||||||
this.tmpActionPath = tmpActionPath;
|
this.tmpActionPath = tmpActionPath;
|
||||||
this.dynamicActionPath = dynamicActionPath;
|
this.dynamicActionPath = dynamicActionPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String normalizeCodeType(String codeType) {
|
public static String normalizeCodeType(String codeType) {
|
||||||
if (codeType == null || codeType.isBlank()) {
|
if (codeType == null || codeType.isBlank()) {
|
||||||
throw new IllegalArgumentException("codeType 不能为空");
|
throw new IllegalArgumentException("codeType 不能为空");
|
||||||
}
|
}
|
||||||
@@ -48,11 +48,11 @@ class ActionSerializer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String buildTmpPath(String actionKey, String codeType) {
|
public String buildTmpPath(String actionKey, String codeType) {
|
||||||
return Path.of(tmpActionPath, System.currentTimeMillis() + "-" + actionKey + normalizeCodeType(codeType)).toString();
|
return Path.of(tmpActionPath, System.currentTimeMillis() + "-" + actionKey + normalizeCodeType(codeType)).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
void tmpSerialize(MetaAction tempAction, String code, String codeType) throws IOException {
|
public void tmpSerialize(MetaAction tempAction, String code, String codeType) throws IOException {
|
||||||
log.debug("行动程序临时序列化: {}", tempAction);
|
log.debug("行动程序临时序列化: {}", tempAction);
|
||||||
Path path = Path.of(tempAction.getLocation());
|
Path path = Path.of(tempAction.getLocation());
|
||||||
validateTmpLocation(path, codeType);
|
validateTmpLocation(path, codeType);
|
||||||
@@ -70,7 +70,7 @@ class ActionSerializer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void persistSerialize(MetaActionInfo metaActionInfo, ActionFileMetaData fileMetaData) {
|
public void persistSerialize(MetaActionInfo metaActionInfo, ActionFileMetaData fileMetaData) {
|
||||||
log.debug("行动程序持久序列化: {}", metaActionInfo);
|
log.debug("行动程序持久序列化: {}", metaActionInfo);
|
||||||
val baseDir = Path.of(dynamicActionPath);
|
val baseDir = Path.of(dynamicActionPath);
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package work.slhaf.partner.core.action.runner;
|
package work.slhaf.partner.core.action.runner.support;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@@ -12,45 +12,46 @@ import java.util.stream.Stream;
|
|||||||
import static java.nio.file.StandardWatchEventKinds.*;
|
import static java.nio.file.StandardWatchEventKinds.*;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
class DirectoryWatchSupport implements Closeable {
|
public class DirectoryWatchSupport implements Closeable {
|
||||||
|
|
||||||
private final Context ctx;
|
private final Context ctx;
|
||||||
private final Map<WatchEvent.Kind<?>, EventHandler> handlers = new HashMap<>();
|
private final Map<WatchEvent.Kind<?>, EventHandler> handlers = new HashMap<>();
|
||||||
private final ExecutorService executor;
|
private final ExecutorService executor;
|
||||||
private final boolean watchAll;
|
private final boolean watchAll;
|
||||||
private final InitLoader initLoader;
|
private final InitLoader initLoader;
|
||||||
DirectoryWatchSupport(Context ctx, ExecutorService executor, boolean watchAll, InitLoader initLoader) {
|
|
||||||
|
public DirectoryWatchSupport(Context ctx, ExecutorService executor, boolean watchAll, InitLoader initLoader) {
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
this.executor = executor;
|
this.executor = executor;
|
||||||
this.watchAll = watchAll;
|
this.watchAll = watchAll;
|
||||||
this.initLoader = initLoader;
|
this.initLoader = initLoader;
|
||||||
}
|
}
|
||||||
|
|
||||||
DirectoryWatchSupport onCreate(EventHandler handler) {
|
public DirectoryWatchSupport onCreate(EventHandler handler) {
|
||||||
ctx.kinds().add(ENTRY_CREATE);
|
ctx.kinds().add(ENTRY_CREATE);
|
||||||
handlers.put(ENTRY_CREATE, handler);
|
handlers.put(ENTRY_CREATE, handler);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
DirectoryWatchSupport onModify(EventHandler handler) {
|
public DirectoryWatchSupport onModify(EventHandler handler) {
|
||||||
ctx.kinds().add(ENTRY_MODIFY);
|
ctx.kinds().add(ENTRY_MODIFY);
|
||||||
handlers.put(ENTRY_MODIFY, handler);
|
handlers.put(ENTRY_MODIFY, handler);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
DirectoryWatchSupport onDelete(EventHandler handler) {
|
public DirectoryWatchSupport onDelete(EventHandler handler) {
|
||||||
ctx.kinds().add(ENTRY_DELETE);
|
ctx.kinds().add(ENTRY_DELETE);
|
||||||
handlers.put(ENTRY_DELETE, handler);
|
handlers.put(ENTRY_DELETE, handler);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
DirectoryWatchSupport onOverflow(EventHandler handler) {
|
public DirectoryWatchSupport onOverflow(EventHandler handler) {
|
||||||
ctx.kinds().add(OVERFLOW);
|
ctx.kinds().add(OVERFLOW);
|
||||||
handlers.put(OVERFLOW, handler);
|
handlers.put(OVERFLOW, handler);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
void start() {
|
public void start() {
|
||||||
registerPath();
|
registerPath();
|
||||||
if (initLoader != null) {
|
if (initLoader != null) {
|
||||||
initLoader.load();
|
initLoader.load();
|
||||||
@@ -58,15 +59,15 @@ class DirectoryWatchSupport implements Closeable {
|
|||||||
executor.execute(buildWatchTask());
|
executor.execute(buildWatchTask());
|
||||||
}
|
}
|
||||||
|
|
||||||
Context context() {
|
public Context context() {
|
||||||
return ctx;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isWatching(Path dir) {
|
public boolean isWatching(Path dir) {
|
||||||
return ctx.watchKeys().values().stream().anyMatch(dir::equals);
|
return ctx.watchKeys().values().stream().anyMatch(dir::equals);
|
||||||
}
|
}
|
||||||
|
|
||||||
void registerDirectory(Path dir) throws IOException {
|
public void registerDirectory(Path dir) throws IOException {
|
||||||
if (!java.nio.file.Files.isDirectory(dir) || isWatching(dir)) {
|
if (!java.nio.file.Files.isDirectory(dir) || isWatching(dir)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -148,16 +149,17 @@ class DirectoryWatchSupport implements Closeable {
|
|||||||
ctx.watchKeys().clear();
|
ctx.watchKeys().clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EventHandler {
|
public interface EventHandler {
|
||||||
void handle(Path thisDir, Path context);
|
void handle(Path thisDir, Path context);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InitLoader {
|
public interface InitLoader {
|
||||||
void load();
|
void load();
|
||||||
}
|
}
|
||||||
|
|
||||||
record Context(Path root, WatchService watchService, Map<WatchKey, Path> watchKeys, Set<WatchEvent.Kind<?>> kinds) {
|
public record Context(Path root, WatchService watchService, Map<WatchKey, Path> watchKeys,
|
||||||
Context(Path root) throws IOException {
|
Set<WatchEvent.Kind<?>> kinds) {
|
||||||
|
public Context(Path root) throws IOException {
|
||||||
this(root, FileSystems.getDefault().newWatchService(), new HashMap<>(), new LinkedHashSet<>());
|
this(root, FileSystems.getDefault().newWatchService(), new HashMap<>(), new LinkedHashSet<>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
package work.slhaf.partner.core.action.runner;
|
|
||||||
|
|
||||||
import io.modelcontextprotocol.client.McpSyncClient;
|
|
||||||
import io.modelcontextprotocol.json.McpJsonMapper;
|
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
|
||||||
import org.junit.jupiter.api.Assertions;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
|
||||||
import org.mockito.Mockito;
|
|
||||||
import work.slhaf.partner.core.action.entity.MetaAction;
|
|
||||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
|
|
||||||
class RunnerStabilizationTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void actionSerializerUsesNormalizedCodeType(@TempDir Path tempDir) throws Exception {
|
|
||||||
ActionSerializer serializer = new ActionSerializer(tempDir.toString(), tempDir.toString());
|
|
||||||
String builtPath = serializer.buildTmpPath("demo", "py");
|
|
||||||
Assertions.assertTrue(builtPath.endsWith(".py"));
|
|
||||||
|
|
||||||
MetaAction metaAction = new MetaAction("demo", false, MetaAction.Type.ORIGIN, builtPath);
|
|
||||||
serializer.tmpSerialize(metaAction, "print('ok')", ".py");
|
|
||||||
|
|
||||||
Assertions.assertTrue(Files.exists(Path.of(builtPath)));
|
|
||||||
Assertions.assertEquals("print('ok')", Files.readString(Path.of(builtPath)));
|
|
||||||
Assertions.assertThrows(Exception.class, () -> serializer.tmpSerialize(metaAction, "print('bad')", ".sh"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void mcpTransportConfigHasValueEquality() {
|
|
||||||
McpTransportConfig.Stdio left = new McpTransportConfig.Stdio(30, "npx", Map.of("A", "1"), List.of("-y", "demo"));
|
|
||||||
McpTransportConfig.Stdio right = new McpTransportConfig.Stdio(30, "npx", Map.of("A", "1"), List.of("-y", "demo"));
|
|
||||||
|
|
||||||
Assertions.assertEquals(left, right);
|
|
||||||
Assertions.assertEquals(left.hashCode(), right.hashCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void mcpConfigWatcherReadParamsAcceptsTimeout(@TempDir Path tempDir) throws Exception {
|
|
||||||
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions = new ConcurrentHashMap<>();
|
|
||||||
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
|
|
||||||
McpConfigWatcher watcher = new McpConfigWatcher(
|
|
||||||
tempDir,
|
|
||||||
existedMetaActions,
|
|
||||||
new McpClientRegistry(),
|
|
||||||
new McpTransportFactory(),
|
|
||||||
new McpMetaRegistry(existedMetaActions),
|
|
||||||
executor
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
Method readParams = McpConfigWatcher.class.getDeclaredMethod("readParams", cn.hutool.json.JSONObject.class);
|
|
||||||
readParams.setAccessible(true);
|
|
||||||
|
|
||||||
cn.hutool.json.JSONObject stdioJson = cn.hutool.json.JSONUtil.parseObj("""
|
|
||||||
{
|
|
||||||
"command": "npx",
|
|
||||||
"args": ["-y", "demo"],
|
|
||||||
"env": {},
|
|
||||||
"timeout": 45
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
Object stdioConfig = readParams.invoke(watcher, stdioJson);
|
|
||||||
|
|
||||||
Assertions.assertInstanceOf(McpTransportConfig.Stdio.class, stdioConfig);
|
|
||||||
Assertions.assertEquals(45, ((McpTransportConfig.Stdio) stdioConfig).timeout());
|
|
||||||
} finally {
|
|
||||||
watcher.close();
|
|
||||||
executor.shutdownNow();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void localRunnerClientCloseIsIdempotent(@TempDir Path tempDir) {
|
|
||||||
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions = new ConcurrentHashMap<>();
|
|
||||||
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
|
|
||||||
LocalRunnerClient client = new LocalRunnerClient(existedMetaActions, executor, tempDir.toString());
|
|
||||||
try {
|
|
||||||
client.close();
|
|
||||||
client.close();
|
|
||||||
} finally {
|
|
||||||
executor.shutdownNow();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void mcpActionExecutorUsesStructuredContentThenTextContent() {
|
|
||||||
McpClientRegistry registry = new McpClientRegistry();
|
|
||||||
McpSyncClient client = Mockito.mock(McpSyncClient.class);
|
|
||||||
registry.register("demo", client);
|
|
||||||
|
|
||||||
McpActionExecutor executor = new McpActionExecutor(registry);
|
|
||||||
MetaAction metaAction = new MetaAction("tool", false, MetaAction.Type.MCP, "demo");
|
|
||||||
|
|
||||||
Mockito.when(client.callTool(Mockito.any())).thenReturn(
|
|
||||||
new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("hello")), null, null, Map.of())
|
|
||||||
);
|
|
||||||
RunnerClient.RunnerResponse textResponse = executor.run(metaAction);
|
|
||||||
Assertions.assertTrue(textResponse.isOk());
|
|
||||||
Assertions.assertEquals("hello", textResponse.getData());
|
|
||||||
|
|
||||||
Mockito.when(client.callTool(Mockito.any())).thenReturn(
|
|
||||||
new McpSchema.CallToolResult(List.of(), Boolean.FALSE, Map.of("k", "v"), Map.of())
|
|
||||||
);
|
|
||||||
RunnerClient.RunnerResponse structuredResponse = executor.run(metaAction);
|
|
||||||
Assertions.assertTrue(structuredResponse.isOk());
|
|
||||||
Assertions.assertEquals("{k=v}", structuredResponse.getData());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void mcpMetaRegistryFallsBackToOriginalToolMetaAfterDescRemoval(@TempDir Path tempDir) throws Exception {
|
|
||||||
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions = new ConcurrentHashMap<>();
|
|
||||||
McpMetaRegistry registry = new McpMetaRegistry(existedMetaActions);
|
|
||||||
try {
|
|
||||||
McpSchema.Tool tool = McpSchema.Tool.builder()
|
|
||||||
.name("tool")
|
|
||||||
.description("tool description")
|
|
||||||
.inputSchema(McpJsonMapper.getDefault(), "{\"type\":\"object\",\"properties\":{}}")
|
|
||||||
.outputSchema(Map.of("type", "string"))
|
|
||||||
.meta(Map.of("io", true, "pre", List.of("pre"), "post", List.of("post"), "strict", true, "tag", List.of("tag")))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
MetaActionInfo baseInfo = registry.buildMetaActionInfo("demo", tool);
|
|
||||||
existedMetaActions.put("demo::tool", baseInfo);
|
|
||||||
|
|
||||||
Path descFile = tempDir.resolve("demo::tool.desc.json");
|
|
||||||
Files.writeString(descFile, """
|
|
||||||
{
|
|
||||||
"io": false,
|
|
||||||
"params": {},
|
|
||||||
"description": "desc override",
|
|
||||||
"tags": ["desc"],
|
|
||||||
"preActions": [],
|
|
||||||
"postActions": [],
|
|
||||||
"strictDependencies": false,
|
|
||||||
"responseSchema": {}
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
Assertions.assertTrue(registry.addOrUpdate(descFile));
|
|
||||||
Assertions.assertEquals("desc override", existedMetaActions.get("demo::tool").getDescription());
|
|
||||||
|
|
||||||
registry.remove(descFile);
|
|
||||||
MetaActionInfo restoredInfo = existedMetaActions.get("demo::tool");
|
|
||||||
Assertions.assertEquals("tool description", restoredInfo.getDescription());
|
|
||||||
Assertions.assertTrue(restoredInfo.isIo());
|
|
||||||
Assertions.assertEquals(List.of("tag"), restoredInfo.getTags());
|
|
||||||
} finally {
|
|
||||||
registry.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user