mirror of https://gitee.com/dromara/liteFlow
test(agent): cover react-agent feature scenarios end-to-end
Add focused feature suites under react-agent test resources for conversation/agentKey isolation, compatible-custom platform handshake, managed shell mode, workspace file tools, skills loading, and a live platform connectivity probe — each with its own EL chain, properties, and prepare/record components. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
65a6213b72
commit
2011d6898b
|
|
@ -0,0 +1,60 @@
|
|||
package com.yomahub.liteflow.test.agent.features.compatiblecustom;
|
||||
|
||||
import com.yomahub.liteflow.core.FlowExecutor;
|
||||
import com.yomahub.liteflow.flow.LiteflowResponse;
|
||||
import com.yomahub.liteflow.property.LiteflowConfig;
|
||||
import com.yomahub.liteflow.property.agent.ShellMode;
|
||||
import com.yomahub.liteflow.test.agent.features.support.CompatibleCustomEchoAgentComponent;
|
||||
import com.yomahub.liteflow.test.agent.features.support.ReActAgentFeatureTestSupport;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 覆盖 guide 中“自定义 OpenAI 兼容厂商”的基础接入方式。
|
||||
*
|
||||
* <p>测试链路固定为 {@code THEN(prepare, agent, record)},确保 Agent 组件是作为
|
||||
* LiteFlow 节点参与整体 EL 编排,而不是脱离 LiteFlow 单独调用。
|
||||
*/
|
||||
@TestPropertySource(value = "classpath:/agent/features/compatiblecustom/application.properties")
|
||||
@SpringBootTest(classes = CompatibleCustomFeatureTest.class)
|
||||
@EnableAutoConfiguration
|
||||
@ComponentScan({ "com.yomahub.liteflow.test.agent.features.compatiblecustom.cmp" })
|
||||
public class CompatibleCustomFeatureTest {
|
||||
|
||||
@Resource
|
||||
private FlowExecutor flowExecutor;
|
||||
|
||||
@Resource
|
||||
private LiteflowConfig liteflowConfig;
|
||||
|
||||
@BeforeEach
|
||||
public void reset() throws Exception {
|
||||
ReActAgentFeatureTestSupport.ensureAgentConfig(
|
||||
liteflowConfig,
|
||||
"target/wk_react_agent_compatiblecustom",
|
||||
false,
|
||||
null,
|
||||
ShellMode.DISABLED);
|
||||
ReActAgentFeatureTestSupport.resetAgentSessionManager();
|
||||
CompatibleCustomEchoAgentComponent.resetCompatibleProbe();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompatibleCustomAgentRunsInThenChain() {
|
||||
LiteflowResponse response = flowExecutor.execute2Resp(
|
||||
"compatibleCustomFeatureChain", "hello-compatible-custom");
|
||||
|
||||
Assertions.assertTrue(response.isSuccess(), response.getMessage());
|
||||
Assertions.assertEquals(1, CompatibleCustomEchoAgentComponent.COMPATIBLE_SPEC_RESOLVE_COUNT.get(),
|
||||
"首次构建 Agent 时应解析一次 compatible-custom ModelSpec");
|
||||
Assertions.assertTrue(response.getSlot().getOutput("compatibleCustomRecord").toString()
|
||||
.contains("hello-compatible-custom"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.yomahub.liteflow.test.agent.features.compatiblecustom.cmp;
|
||||
|
||||
import com.yomahub.liteflow.test.agent.features.support.CompatibleCustomEchoAgentComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 通过 compatible-custom 配置解析构建的测试 Agent,执行时使用本地 Echo 模型。
|
||||
*/
|
||||
@Component("compatibleCustomAgent")
|
||||
public class CompatibleCustomAgentCmp extends CompatibleCustomEchoAgentComponent {
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.yomahub.liteflow.test.agent.features.compatiblecustom.cmp;
|
||||
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* compatible-custom 功能包的准备节点。
|
||||
*
|
||||
* <p>把 LiteFlow 入参写入 chainReqData,供 Agent 组件的 userPrompt() 读取。
|
||||
*/
|
||||
@Component("compatibleCustomPrepare")
|
||||
public class CompatibleCustomPrepareCmp extends NodeComponent {
|
||||
@Override
|
||||
public void process() {
|
||||
getSlot().setChainReqData(getSlot().getChainId(), getRequestData());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.yomahub.liteflow.test.agent.features.compatiblecustom.cmp;
|
||||
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* compatible-custom 功能包的记录节点。
|
||||
*/
|
||||
@Component("compatibleCustomRecord")
|
||||
public class CompatibleCustomRecordCmp extends NodeComponent {
|
||||
@Override
|
||||
public void process() {
|
||||
getSlot().setOutput(getNodeId(), getSlot().getResponseData());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.yomahub.liteflow.test.agent.features.conversation;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* conversation 功能包的测试观测点。
|
||||
*/
|
||||
public final class ConversationFeatureProbe {
|
||||
public static final AtomicReference<String> AGENT_A_CONVERSATION_ID = new AtomicReference<>();
|
||||
public static final AtomicReference<String> AGENT_B_CONVERSATION_ID = new AtomicReference<>();
|
||||
public static final AtomicReference<String> AGENT_A_KEY = new AtomicReference<>();
|
||||
public static final AtomicReference<String> AGENT_B_KEY = new AtomicReference<>();
|
||||
public static final AtomicReference<String> AGENT_A_WORKSPACE = new AtomicReference<>();
|
||||
public static final AtomicReference<String> AGENT_B_WORKSPACE = new AtomicReference<>();
|
||||
|
||||
private ConversationFeatureProbe() {
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
AGENT_A_CONVERSATION_ID.set(null);
|
||||
AGENT_B_CONVERSATION_ID.set(null);
|
||||
AGENT_A_KEY.set(null);
|
||||
AGENT_B_KEY.set(null);
|
||||
AGENT_A_WORKSPACE.set(null);
|
||||
AGENT_B_WORKSPACE.set(null);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package com.yomahub.liteflow.test.agent.features.conversation;
|
||||
|
||||
import com.yomahub.liteflow.agent.component.ReActAgentComponent;
|
||||
import com.yomahub.liteflow.core.FlowExecutor;
|
||||
import com.yomahub.liteflow.flow.LiteflowResponse;
|
||||
import com.yomahub.liteflow.property.LiteflowConfig;
|
||||
import com.yomahub.liteflow.property.agent.ShellMode;
|
||||
import com.yomahub.liteflow.test.agent.features.support.CompatibleCustomEchoAgentComponent;
|
||||
import com.yomahub.liteflow.test.agent.features.support.ReActAgentFeatureTestSupport;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 覆盖 guide 中 conversationId、agentKey 与 workspace 的协作边界。
|
||||
*
|
||||
* <p>链路使用 {@code THEN(agentA, agentB)},两个 Agent 默认使用不同 nodeId 作为
|
||||
* agentKey,但会继承同一个 conversationId,因此应共享同一个 workspace 子目录。
|
||||
*/
|
||||
@TestPropertySource(value = "classpath:/agent/features/conversation/application.properties")
|
||||
@SpringBootTest(classes = ConversationFeatureTest.class)
|
||||
@EnableAutoConfiguration
|
||||
@ComponentScan({ "com.yomahub.liteflow.test.agent.features.conversation.cmp" })
|
||||
public class ConversationFeatureTest {
|
||||
|
||||
private static final String RAW_CONVERSATION_ID = "chat/user 1";
|
||||
|
||||
@Resource
|
||||
private FlowExecutor flowExecutor;
|
||||
|
||||
@Resource
|
||||
private LiteflowConfig liteflowConfig;
|
||||
|
||||
@BeforeEach
|
||||
public void reset() throws Exception {
|
||||
ReActAgentFeatureTestSupport.ensureAgentConfig(
|
||||
liteflowConfig,
|
||||
"target/wk_react_agent_conversation",
|
||||
false,
|
||||
null,
|
||||
ShellMode.DISABLED);
|
||||
ReActAgentFeatureTestSupport.resetAgentSessionManager();
|
||||
CompatibleCustomEchoAgentComponent.resetCompatibleProbe();
|
||||
ConversationFeatureProbe.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConversationIdIsSanitizedAndSharedAcrossThenAgents() {
|
||||
LiteflowResponse response = flowExecutor.execute2Resp("conversationFeatureChain", Map.of(
|
||||
ReActAgentComponent.CONVERSATION_ID_REQUEST_KEY, RAW_CONVERSATION_ID,
|
||||
"prompt", "share workspace"));
|
||||
|
||||
Assertions.assertTrue(response.isSuccess(), response.getMessage());
|
||||
Assertions.assertEquals(2, CompatibleCustomEchoAgentComponent.COMPATIBLE_SPEC_RESOLVE_COUNT.get(),
|
||||
"两个不同 agentKey 的 Agent 应分别构建各自的 compatible-custom 模型");
|
||||
Assertions.assertNotEquals(RAW_CONVERSATION_ID, ConversationFeatureProbe.AGENT_A_CONVERSATION_ID.get(),
|
||||
"ctx 中的 conversationId 应使用安全化后的目录名");
|
||||
Assertions.assertEquals(ConversationFeatureProbe.AGENT_A_CONVERSATION_ID.get(),
|
||||
ConversationFeatureProbe.AGENT_B_CONVERSATION_ID.get(),
|
||||
"同一条 THEN 链路内后续 Agent 应复用首个 Agent 的 conversation");
|
||||
Assertions.assertEquals(ConversationFeatureProbe.AGENT_A_WORKSPACE.get(),
|
||||
ConversationFeatureProbe.AGENT_B_WORKSPACE.get(),
|
||||
"同一 conversation 下的多个 agentKey 应共享 workspace");
|
||||
Assertions.assertEquals("conversationAgentA", ConversationFeatureProbe.AGENT_A_KEY.get());
|
||||
Assertions.assertEquals("conversationAgentB", ConversationFeatureProbe.AGENT_B_KEY.get());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.yomahub.liteflow.test.agent.features.conversation.cmp;
|
||||
|
||||
import com.yomahub.liteflow.test.agent.features.conversation.ConversationFeatureProbe;
|
||||
import com.yomahub.liteflow.test.agent.features.support.CompatibleCustomEchoAgentComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 第一个 conversation Agent,负责触发 conversation 解析并记录安全化后的上下文。
|
||||
*/
|
||||
@Component("conversationAgentA")
|
||||
public class ConversationAgentACmp extends CompatibleCustomEchoAgentComponent {
|
||||
@Override
|
||||
protected String userPrompt() {
|
||||
ConversationFeatureProbe.AGENT_A_CONVERSATION_ID.set(ctx().getConversationId());
|
||||
ConversationFeatureProbe.AGENT_A_KEY.set(ctx().getAgentKey());
|
||||
ConversationFeatureProbe.AGENT_A_WORKSPACE.set(ctx().getWorkspaceDir().toString());
|
||||
return super.userPrompt();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.yomahub.liteflow.test.agent.features.conversation.cmp;
|
||||
|
||||
import com.yomahub.liteflow.test.agent.features.conversation.ConversationFeatureProbe;
|
||||
import com.yomahub.liteflow.test.agent.features.support.CompatibleCustomEchoAgentComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 第二个 conversation Agent,不覆写 resolveConversationId(),用于验证 slot 中的 conversation 复用。
|
||||
*/
|
||||
@Component("conversationAgentB")
|
||||
public class ConversationAgentBCmp extends CompatibleCustomEchoAgentComponent {
|
||||
@Override
|
||||
protected String userPrompt() {
|
||||
ConversationFeatureProbe.AGENT_B_CONVERSATION_ID.set(ctx().getConversationId());
|
||||
ConversationFeatureProbe.AGENT_B_KEY.set(ctx().getAgentKey());
|
||||
ConversationFeatureProbe.AGENT_B_WORKSPACE.set(ctx().getWorkspaceDir().toString());
|
||||
return super.userPrompt();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.yomahub.liteflow.test.agent.features.conversation.cmp;
|
||||
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 保留原始请求 Map,使默认 resolveConversationId() 能读取约定的 conversationId 字段。
|
||||
*/
|
||||
@Component("conversationPrepare")
|
||||
public class ConversationPrepareCmp extends NodeComponent {
|
||||
@Override
|
||||
public void process() {
|
||||
getSlot().setChainReqData(getSlot().getChainId(), getRequestData());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.yomahub.liteflow.test.agent.features.conversation.cmp;
|
||||
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* conversation 功能包的记录节点。
|
||||
*/
|
||||
@Component("conversationRecord")
|
||||
public class ConversationRecordCmp extends NodeComponent {
|
||||
@Override
|
||||
public void process() {
|
||||
getSlot().setOutput(getNodeId(), getSlot().getResponseData());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package com.yomahub.liteflow.test.agent.features.platform;
|
||||
|
||||
import com.yomahub.liteflow.core.FlowExecutor;
|
||||
import com.yomahub.liteflow.flow.LiteflowResponse;
|
||||
import com.yomahub.liteflow.property.LiteflowConfig;
|
||||
import com.yomahub.liteflow.property.agent.AgentConfig;
|
||||
import com.yomahub.liteflow.property.agent.PlatformCredential;
|
||||
import com.yomahub.liteflow.property.agent.ShellMode;
|
||||
import com.yomahub.liteflow.test.agent.features.support.ReActAgentFeatureTestSupport;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 覆盖 guide 中“测试者提供 baseUrl 和 key 后访问 OpenAI 兼容平台”的真实冒烟场景。
|
||||
*
|
||||
* <p>该测试默认跳过。运行者需要提供:
|
||||
* {@code TEST_LITEFLOW_COMPATIBLE_CUSTOM_API_KEY}、
|
||||
* {@code TEST_LITEFLOW_COMPATIBLE_CUSTOM_BASE_URL},以及可选的
|
||||
* {@code TEST_LITEFLOW_COMPATIBLE_CUSTOM_MODEL}。
|
||||
*/
|
||||
@TestPropertySource(value = "classpath:/agent/features/platform/application.properties")
|
||||
@SpringBootTest(classes = CompatibleCustomPlatformConnectivityTest.class)
|
||||
@EnableAutoConfiguration
|
||||
@ComponentScan({ "com.yomahub.liteflow.test.agent.features.platform.cmp" })
|
||||
public class CompatibleCustomPlatformConnectivityTest {
|
||||
|
||||
@Resource
|
||||
private FlowExecutor flowExecutor;
|
||||
|
||||
@Resource
|
||||
private LiteflowConfig liteflowConfig;
|
||||
|
||||
@BeforeEach
|
||||
public void reset() throws Exception {
|
||||
ensureMinimalAgentConfigWithoutFakeCredential();
|
||||
ReActAgentFeatureTestSupport.resetAgentSessionManager();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompatibleCustomPlatformConnectivityWhenCredentialProvided() {
|
||||
PlatformCredential credential = liteflowConfig.getAgent().getOpenaiCompatible()
|
||||
.get(ReActAgentFeatureTestSupport.COMPATIBLE_CONFIG_KEY);
|
||||
Assumptions.assumeTrue(credential != null
|
||||
&& credential.getApiKey() != null
|
||||
&& !credential.getApiKey().isBlank()
|
||||
&& credential.getBaseUrl() != null
|
||||
&& !credential.getBaseUrl().isBlank(),
|
||||
"compatible-custom api-key/base-url 未配置,跳过真实平台冒烟测试");
|
||||
|
||||
LiteflowResponse response = flowExecutor.execute2Resp(
|
||||
"compatibleCustomPlatformChain",
|
||||
"用一句中文短句回复:LiteFlow ReAct Agent 连通性正常。");
|
||||
|
||||
Assertions.assertTrue(response.isSuccess(), response.getMessage());
|
||||
Object reply = response.getSlot().getOutput("platformRecord");
|
||||
Assertions.assertNotNull(reply, "真实平台应返回非空回复");
|
||||
Assertions.assertFalse(reply.toString().isBlank(), "真实平台回复不应为空白");
|
||||
}
|
||||
|
||||
private void ensureMinimalAgentConfigWithoutFakeCredential() {
|
||||
if (liteflowConfig.getAgent() == null) {
|
||||
liteflowConfig.setAgent(new AgentConfig());
|
||||
}
|
||||
AgentConfig agentConfig = liteflowConfig.getAgent();
|
||||
agentConfig.getWorkspace().setRoot("target/wk_react_agent_platform");
|
||||
agentConfig.getShell().setMode(ShellMode.DISABLED);
|
||||
agentConfig.getDefaults().setMaxIterations(3);
|
||||
agentConfig.getLogging().setReactEnabled(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.yomahub.liteflow.test.agent.features.platform.cmp;
|
||||
|
||||
import com.yomahub.liteflow.agent.component.ReActAgentComponent;
|
||||
import com.yomahub.liteflow.agent.model.ModelSpec;
|
||||
import com.yomahub.liteflow.agent.openai.OpenAICompatible;
|
||||
import com.yomahub.liteflow.test.agent.features.support.ReActAgentFeatureTestSupport;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 真实 compatible-custom Agent。该组件不覆写 buildModel(),会真正使用 OpenAI 兼容端点。
|
||||
*/
|
||||
@Component("compatibleCustomLiveAgent")
|
||||
public class CompatibleCustomLiveAgentCmp extends ReActAgentComponent {
|
||||
|
||||
@Value("${test.compatible-custom.model:gpt-4o-mini}")
|
||||
private String modelName;
|
||||
|
||||
@Override
|
||||
protected ModelSpec<?> model() {
|
||||
return OpenAICompatible.custom(
|
||||
ReActAgentFeatureTestSupport.COMPATIBLE_CONFIG_KEY,
|
||||
modelName)
|
||||
.temperature(0.1)
|
||||
.maxTokens(64);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String systemPrompt() {
|
||||
return "你是 LiteFlow ReAct Agent 的连通性测试助手,只能用一句中文回复。";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String userPrompt() {
|
||||
Object reqData = getSlot().getChainReqData(getSlot().getChainId());
|
||||
return reqData == null ? "" : reqData.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int maxIterations() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean enableShellTool() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean enableWorkspaceFileTools() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.yomahub.liteflow.test.agent.features.platform.cmp;
|
||||
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* platform 功能包的准备节点。
|
||||
*/
|
||||
@Component("platformPrepare")
|
||||
public class PlatformPrepareCmp extends NodeComponent {
|
||||
@Override
|
||||
public void process() {
|
||||
getSlot().setChainReqData(getSlot().getChainId(), getRequestData());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.yomahub.liteflow.test.agent.features.platform.cmp;
|
||||
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* platform 功能包的记录节点。
|
||||
*/
|
||||
@Component("platformRecord")
|
||||
public class PlatformRecordCmp extends NodeComponent {
|
||||
@Override
|
||||
public void process() {
|
||||
getSlot().setOutput(getNodeId(), getSlot().getResponseData());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.yomahub.liteflow.test.agent.features.shell;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* shell 功能包的测试观测点。
|
||||
*/
|
||||
public final class ShellFeatureProbe {
|
||||
public static final AtomicReference<String> WORKSPACE = new AtomicReference<>();
|
||||
public static final AtomicReference<String> PWD_OUTPUT = new AtomicReference<>();
|
||||
public static final AtomicReference<String> DENIED_OUTPUT = new AtomicReference<>();
|
||||
|
||||
private ShellFeatureProbe() {
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
WORKSPACE.set(null);
|
||||
PWD_OUTPUT.set(null);
|
||||
DENIED_OUTPUT.set(null);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.yomahub.liteflow.test.agent.features.shell;
|
||||
|
||||
import com.yomahub.liteflow.agent.tool.ManagedShellCommandTool;
|
||||
import com.yomahub.liteflow.core.FlowExecutor;
|
||||
import com.yomahub.liteflow.flow.LiteflowResponse;
|
||||
import com.yomahub.liteflow.property.LiteflowConfig;
|
||||
import com.yomahub.liteflow.property.agent.ShellMode;
|
||||
import com.yomahub.liteflow.test.agent.features.support.CompatibleCustomEchoAgentComponent;
|
||||
import com.yomahub.liteflow.test.agent.features.support.ReActAgentFeatureTestSupport;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 覆盖 guide 中受管 Shell 工具的安全配置。
|
||||
*
|
||||
* <p>配置为 WHITELIST,仅允许 {@code pwd}。测试验证命令在当前 conversation workspace
|
||||
* 中执行,并确认未列入白名单的命令会被拒绝。
|
||||
*/
|
||||
@TestPropertySource(value = "classpath:/agent/features/shell/application.properties")
|
||||
@SpringBootTest(classes = ShellFeatureTest.class)
|
||||
@EnableAutoConfiguration
|
||||
@ComponentScan({ "com.yomahub.liteflow.test.agent.features.shell.cmp" })
|
||||
public class ShellFeatureTest {
|
||||
|
||||
@Resource
|
||||
private FlowExecutor flowExecutor;
|
||||
|
||||
@Resource
|
||||
private LiteflowConfig liteflowConfig;
|
||||
|
||||
@BeforeEach
|
||||
public void reset() throws Exception {
|
||||
ReActAgentFeatureTestSupport.ensureAgentConfig(
|
||||
liteflowConfig,
|
||||
"target/wk_react_agent_shell",
|
||||
false,
|
||||
null,
|
||||
ShellMode.WHITELIST);
|
||||
liteflowConfig.getAgent().getShell().setWhitelist(List.of("pwd"));
|
||||
ReActAgentFeatureTestSupport.resetAgentSessionManager();
|
||||
CompatibleCustomEchoAgentComponent.resetCompatibleProbe();
|
||||
ShellFeatureProbe.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testManagedShellRunsInWorkspaceAndRejectsNonWhitelistedCommand() {
|
||||
LiteflowResponse response = flowExecutor.execute2Resp("shellFeatureChain", "shell-tools");
|
||||
|
||||
Assertions.assertTrue(response.isSuccess(), response.getMessage());
|
||||
Assertions.assertEquals(ShellFeatureProbe.WORKSPACE.get(), ShellFeatureProbe.PWD_OUTPUT.get().trim(),
|
||||
"pwd 应在当前 conversation workspace 目录下执行");
|
||||
Assertions.assertTrue(ShellFeatureProbe.DENIED_OUTPUT.get().contains("not allowed by whitelist"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.yomahub.liteflow.test.agent.features.shell.cmp;
|
||||
|
||||
import com.yomahub.liteflow.agent.tool.ManagedShellCommandTool;
|
||||
import com.yomahub.liteflow.test.agent.features.shell.ShellFeatureProbe;
|
||||
import com.yomahub.liteflow.test.agent.features.support.CompatibleCustomEchoAgentComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 直接调用受管 Shell 工具,避免依赖模型是否主动选择工具。
|
||||
*/
|
||||
@Component("shellAgent")
|
||||
public class ShellAgentCmp extends CompatibleCustomEchoAgentComponent {
|
||||
@Override
|
||||
protected String userPrompt() {
|
||||
ManagedShellCommandTool tool = new ManagedShellCommandTool(ctx().getWorkspaceDir(), agentConfig());
|
||||
ShellFeatureProbe.WORKSPACE.set(ctx().getWorkspaceDir().toAbsolutePath().normalize().toString());
|
||||
ShellFeatureProbe.PWD_OUTPUT.set(tool.executeCommand("pwd"));
|
||||
ShellFeatureProbe.DENIED_OUTPUT.set(tool.executeCommand("echo denied"));
|
||||
return super.userPrompt();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.yomahub.liteflow.test.agent.features.shell.cmp;
|
||||
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* shell 功能包的准备节点。
|
||||
*/
|
||||
@Component("shellPrepare")
|
||||
public class ShellPrepareCmp extends NodeComponent {
|
||||
@Override
|
||||
public void process() {
|
||||
getSlot().setChainReqData(getSlot().getChainId(), getRequestData());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.yomahub.liteflow.test.agent.features.shell.cmp;
|
||||
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* shell 功能包的记录节点。
|
||||
*/
|
||||
@Component("shellRecord")
|
||||
public class ShellRecordCmp extends NodeComponent {
|
||||
@Override
|
||||
public void process() {
|
||||
getSlot().setOutput(getNodeId(), getSlot().getResponseData());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.yomahub.liteflow.test.agent.features.skills;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* skills 功能包的测试观测点。
|
||||
*/
|
||||
public final class SkillsFeatureProbe {
|
||||
public static final AtomicReference<List<String>> USED_SKILLS = new AtomicReference<>(List.of());
|
||||
public static final AtomicReference<List<String>> TOOL_NAMES = new AtomicReference<>(List.of());
|
||||
|
||||
private SkillsFeatureProbe() {
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
USED_SKILLS.set(List.of());
|
||||
TOOL_NAMES.set(new CopyOnWriteArrayList<>());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package com.yomahub.liteflow.test.agent.features.skills;
|
||||
|
||||
import com.yomahub.liteflow.core.FlowExecutor;
|
||||
import com.yomahub.liteflow.flow.LiteflowResponse;
|
||||
import com.yomahub.liteflow.property.LiteflowConfig;
|
||||
import com.yomahub.liteflow.property.agent.ShellMode;
|
||||
import com.yomahub.liteflow.test.agent.features.support.CompatibleCustomEchoAgentComponent;
|
||||
import com.yomahub.liteflow.test.agent.features.support.ReActAgentFeatureTestSupport;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 覆盖 guide 中 Skills 开启、组件级技能过滤,以及 usedSkills() 的记录语义。
|
||||
*
|
||||
* <p>本测试通过本地模型桩返回一次 {@code load_skill_through_path} 工具调用,
|
||||
* 让 AgentScope 真实加载 filesystem skill,再由 LiteFlow 的 {@code usedSkills()} 读取结果。
|
||||
*/
|
||||
@TestPropertySource(value = "classpath:/agent/features/skills/application.properties")
|
||||
@SpringBootTest(classes = SkillsFeatureTest.class)
|
||||
@EnableAutoConfiguration
|
||||
@ComponentScan({ "com.yomahub.liteflow.test.agent.features.skills.cmp" })
|
||||
public class SkillsFeatureTest {
|
||||
|
||||
@Resource
|
||||
private FlowExecutor flowExecutor;
|
||||
|
||||
@Resource
|
||||
private LiteflowConfig liteflowConfig;
|
||||
|
||||
@BeforeEach
|
||||
public void reset() throws Exception {
|
||||
ReActAgentFeatureTestSupport.ensureAgentConfig(
|
||||
liteflowConfig,
|
||||
"target/wk_react_agent_skills",
|
||||
true,
|
||||
resolveSkillsPath(),
|
||||
ShellMode.DISABLED);
|
||||
ReActAgentFeatureTestSupport.resetAgentSessionManager();
|
||||
CompatibleCustomEchoAgentComponent.resetCompatibleProbe();
|
||||
SkillsFeatureProbe.reset();
|
||||
}
|
||||
|
||||
private static String resolveSkillsPath() {
|
||||
Path moduleRelative = Path.of("src/test/resources/agent/features/skills");
|
||||
if (Files.isDirectory(moduleRelative)) {
|
||||
return moduleRelative.toAbsolutePath().normalize().toString();
|
||||
}
|
||||
return Path.of("liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/agent/features/skills")
|
||||
.toAbsolutePath()
|
||||
.normalize()
|
||||
.toString();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUsedSkillsTracksFilesystemSkillLoadedInThenChain() {
|
||||
LiteflowResponse response = flowExecutor.execute2Resp("skillsFeatureChain", "load-feature-skill");
|
||||
|
||||
Assertions.assertTrue(response.isSuccess(), response.getMessage());
|
||||
Assertions.assertEquals(List.of("feature-demo"), SkillsFeatureProbe.USED_SKILLS.get(),
|
||||
"load_skill_through_path 成功后 usedSkills() 应返回技能名");
|
||||
Assertions.assertTrue(SkillsFeatureProbe.TOOL_NAMES.get().contains("load_skill_through_path"),
|
||||
"开启 skills 后 Toolkit 应包含 AgentScope 的技能加载工具");
|
||||
Assertions.assertEquals(1, CompatibleCustomEchoAgentComponent.COMPATIBLE_SPEC_RESOLVE_COUNT.get());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.yomahub.liteflow.test.agent.features.skills.cmp;
|
||||
|
||||
import com.yomahub.liteflow.agent.model.ModelSpec;
|
||||
import com.yomahub.liteflow.agent.openai.OpenAICompatible;
|
||||
import com.yomahub.liteflow.test.agent.features.skills.SkillsFeatureProbe;
|
||||
import com.yomahub.liteflow.test.agent.features.support.CompatibleCustomEchoAgentComponent;
|
||||
import com.yomahub.liteflow.test.agent.features.support.ReActAgentFeatureTestSupport;
|
||||
import io.agentscope.core.message.Msg;
|
||||
import io.agentscope.core.model.Model;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 使用 compatible-custom 配置解析,同时只允许加载 feature-demo 一个技能。
|
||||
*/
|
||||
@Component("skillsAgent")
|
||||
public class SkillsAgentCmp extends CompatibleCustomEchoAgentComponent {
|
||||
@Override
|
||||
protected List<String> skills() {
|
||||
return List.of("feature-demo");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ModelSpec<?> model() {
|
||||
return OpenAICompatible.custom(
|
||||
ReActAgentFeatureTestSupport.COMPATIBLE_CONFIG_KEY,
|
||||
"compatible-custom-skills-test-model");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Model buildModel() {
|
||||
model().resolve(agentConfig());
|
||||
COMPATIBLE_SPEC_RESOLVE_COUNT.incrementAndGet();
|
||||
return new SkillsLoadingModel();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleReply(Msg reply) {
|
||||
SkillsFeatureProbe.USED_SKILLS.set(usedSkills());
|
||||
super.handleReply(reply);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.yomahub.liteflow.test.agent.features.skills.cmp;
|
||||
|
||||
import com.yomahub.liteflow.test.agent.features.skills.SkillsFeatureProbe;
|
||||
import io.agentscope.core.message.Msg;
|
||||
import io.agentscope.core.message.TextBlock;
|
||||
import io.agentscope.core.message.ToolUseBlock;
|
||||
import io.agentscope.core.model.ChatResponse;
|
||||
import io.agentscope.core.model.GenerateOptions;
|
||||
import io.agentscope.core.model.Model;
|
||||
import io.agentscope.core.model.ToolSchema;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* skills 功能包的本地模型桩:首轮请求触发 load_skill_through_path,第二轮返回最终文本。
|
||||
*/
|
||||
public class SkillsLoadingModel implements Model {
|
||||
private final AtomicInteger callCount = new AtomicInteger();
|
||||
|
||||
@Override
|
||||
public Flux<ChatResponse> stream(List<Msg> messages, List<ToolSchema> toolSchemas, GenerateOptions options) {
|
||||
List<String> inputTexts = messages == null ? List.of() : messages.stream()
|
||||
.map(Msg::getTextContent)
|
||||
.toList();
|
||||
List<String> toolNames = toolSchemas == null ? List.of() : toolSchemas.stream()
|
||||
.map(ToolSchema::getName)
|
||||
.toList();
|
||||
SkillsFeatureProbe.TOOL_NAMES.set(toolNames);
|
||||
if (callCount.incrementAndGet() == 1 && inputTexts.contains("load-feature-skill")) {
|
||||
return Flux.just(ChatResponse.builder()
|
||||
.content(List.of(new ToolUseBlock(
|
||||
"load-feature-demo-call",
|
||||
"load_skill_through_path",
|
||||
Map.of("skillId", "feature-demo_filesystem-features_skills", "path", "SKILL.md"),
|
||||
"{\"skillId\":\"feature-demo_filesystem-features_skills\",\"path\":\"SKILL.md\"}",
|
||||
null)))
|
||||
.finishReason("tool_calls")
|
||||
.build());
|
||||
}
|
||||
return Flux.just(ChatResponse.builder()
|
||||
.content(List.of(TextBlock.builder().text("skill-loaded").build()))
|
||||
.finishReason("stop")
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModelName() {
|
||||
return "skill-loading-model";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.yomahub.liteflow.test.agent.features.skills.cmp;
|
||||
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* skills 功能包的准备节点。
|
||||
*/
|
||||
@Component("skillsPrepare")
|
||||
public class SkillsPrepareCmp extends NodeComponent {
|
||||
@Override
|
||||
public void process() {
|
||||
getSlot().setChainReqData(getSlot().getChainId(), getRequestData());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.yomahub.liteflow.test.agent.features.skills.cmp;
|
||||
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* skills 功能包的记录节点。
|
||||
*/
|
||||
@Component("skillsRecord")
|
||||
public class SkillsRecordCmp extends NodeComponent {
|
||||
@Override
|
||||
public void process() {
|
||||
getSlot().setOutput(getNodeId(), getSlot().getResponseData());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.yomahub.liteflow.test.agent.features.support;
|
||||
|
||||
import com.yomahub.liteflow.agent.component.ReActAgentComponent;
|
||||
import com.yomahub.liteflow.agent.model.ModelSpec;
|
||||
import com.yomahub.liteflow.agent.openai.OpenAICompatible;
|
||||
import io.agentscope.core.model.Model;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* 功能测试使用的 OpenAI 兼容模型测试桩。
|
||||
*
|
||||
* <p>组件仍然通过 {@link OpenAICompatible#custom(String, String)} 解析
|
||||
* {@code liteflow.agent.openai-compatible.compatible-custom.*} 配置,覆盖真实入口的
|
||||
* credential 读取路径;真正执行时返回本地 Echo 模型,避免普通功能测试依赖外网。
|
||||
*/
|
||||
public abstract class CompatibleCustomEchoAgentComponent extends ReActAgentComponent {
|
||||
|
||||
public static final AtomicInteger COMPATIBLE_SPEC_RESOLVE_COUNT = new AtomicInteger();
|
||||
|
||||
public static void resetCompatibleProbe() {
|
||||
COMPATIBLE_SPEC_RESOLVE_COUNT.set(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ModelSpec<?> model() {
|
||||
return OpenAICompatible.custom(
|
||||
ReActAgentFeatureTestSupport.COMPATIBLE_CONFIG_KEY,
|
||||
"compatible-custom-test-model")
|
||||
.temperature(0.1)
|
||||
.maxTokens(64)
|
||||
.stream(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Model buildModel() {
|
||||
model().resolve(agentConfig());
|
||||
COMPATIBLE_SPEC_RESOLVE_COUNT.incrementAndGet();
|
||||
return new CompatibleCustomEchoModel(getNodeId());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String systemPrompt() {
|
||||
return "compatible custom test agent: " + getNodeId();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String userPrompt() {
|
||||
Object reqData = getSlot().getChainReqData(getSlot().getChainId());
|
||||
return reqData == null ? "" : reqData.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean enableShellTool() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean enableWorkspaceFileTools() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean enableReActLogging() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.yomahub.liteflow.test.agent.features.support;
|
||||
|
||||
import io.agentscope.core.message.Msg;
|
||||
import io.agentscope.core.message.TextBlock;
|
||||
import io.agentscope.core.model.ChatResponse;
|
||||
import io.agentscope.core.model.GenerateOptions;
|
||||
import io.agentscope.core.model.Model;
|
||||
import io.agentscope.core.model.ToolSchema;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 功能测试使用的本地 Echo 模型,避免普通用例依赖真实平台网络。
|
||||
*/
|
||||
public class CompatibleCustomEchoModel implements Model {
|
||||
|
||||
private final String nodeId;
|
||||
|
||||
public CompatibleCustomEchoModel(String nodeId) {
|
||||
this.nodeId = nodeId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<ChatResponse> stream(List<Msg> messages, List<ToolSchema> toolSchemas, GenerateOptions options) {
|
||||
List<String> inputTexts = messages == null ? List.of() : messages.stream()
|
||||
.map(Msg::getTextContent)
|
||||
.toList();
|
||||
return Flux.just(ChatResponse.builder()
|
||||
.content(List.of(TextBlock.builder()
|
||||
.text("reply:" + nodeId + ":" + inputTexts)
|
||||
.build()))
|
||||
.finishReason("stop")
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModelName() {
|
||||
return "compatible-custom-echo";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.yomahub.liteflow.test.agent.features.support;
|
||||
|
||||
import com.yomahub.liteflow.property.LiteflowConfig;
|
||||
import com.yomahub.liteflow.property.agent.AgentConfig;
|
||||
import com.yomahub.liteflow.property.agent.PlatformCredential;
|
||||
import com.yomahub.liteflow.property.agent.ShellMode;
|
||||
|
||||
/**
|
||||
* 新增 ReAct Agent 功能测试的共享辅助方法。
|
||||
*
|
||||
* <p>这些测试每个功能包都使用独立的 Spring 配置文件,但有两件事需要保持一致:
|
||||
* 1)补齐 SpringBoot 属性绑定在单模块测试中偶发缺失的 agent 段;
|
||||
* 2)每个测试方法开始前清掉 ReActAgentComponent 内部缓存的 SessionManager。
|
||||
*/
|
||||
public final class ReActAgentFeatureTestSupport {
|
||||
|
||||
public static final String COMPATIBLE_CONFIG_KEY = "compatible-custom";
|
||||
public static final String DEFAULT_COMPATIBLE_API_KEY = "test-compatible-key";
|
||||
public static final String DEFAULT_COMPATIBLE_BASE_URL = "http://127.0.0.1:65535/v1";
|
||||
|
||||
private ReActAgentFeatureTestSupport() {
|
||||
}
|
||||
|
||||
public static void ensureAgentConfig(
|
||||
LiteflowConfig liteflowConfig,
|
||||
String workspaceRoot,
|
||||
boolean skillsEnabled,
|
||||
String skillsPath,
|
||||
ShellMode shellMode) {
|
||||
if (liteflowConfig.getAgent() == null) {
|
||||
liteflowConfig.setAgent(new AgentConfig());
|
||||
}
|
||||
AgentConfig agentConfig = liteflowConfig.getAgent();
|
||||
agentConfig.getWorkspace().setRoot(workspaceRoot);
|
||||
agentConfig.getShell().setMode(shellMode);
|
||||
agentConfig.getDefaults().setMaxIterations(6);
|
||||
agentConfig.getLogging().setReactEnabled(false);
|
||||
agentConfig.getSkills().setEnabled(skillsEnabled);
|
||||
if (skillsPath != null) {
|
||||
agentConfig.getSkills().setPath(skillsPath);
|
||||
}
|
||||
agentConfig.getSkills().setStrict(true);
|
||||
|
||||
PlatformCredential credential = agentConfig.getOpenaiCompatible()
|
||||
.computeIfAbsent(COMPATIBLE_CONFIG_KEY, key -> new PlatformCredential());
|
||||
if (credential.getApiKey() == null || credential.getApiKey().isBlank()) {
|
||||
credential.setApiKey(DEFAULT_COMPATIBLE_API_KEY);
|
||||
}
|
||||
if (credential.getBaseUrl() == null || credential.getBaseUrl().isBlank()) {
|
||||
credential.setBaseUrl(DEFAULT_COMPATIBLE_BASE_URL);
|
||||
}
|
||||
}
|
||||
|
||||
public static void resetAgentSessionManager() throws Exception {
|
||||
Class<?> holder = Class.forName(
|
||||
"com.yomahub.liteflow.agent.component.ReActAgentComponent$AgentSessionManagerHolder");
|
||||
var reset = holder.getDeclaredMethod("resetForTesting");
|
||||
reset.setAccessible(true);
|
||||
reset.invoke(null);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.yomahub.liteflow.test.agent.features.workspace;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* workspace 功能包的测试观测点。
|
||||
*/
|
||||
public final class WorkspaceFeatureProbe {
|
||||
public static final AtomicReference<String> TRUNCATED_READ = new AtomicReference<>();
|
||||
public static final AtomicReference<List<String>> LIST_RESULT = new AtomicReference<>(List.of());
|
||||
public static final AtomicReference<String> RELATIVE_ESCAPE_DENIED = new AtomicReference<>();
|
||||
public static final AtomicReference<String> ABSOLUTE_ESCAPE_DENIED = new AtomicReference<>();
|
||||
|
||||
private WorkspaceFeatureProbe() {
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
TRUNCATED_READ.set(null);
|
||||
LIST_RESULT.set(List.of());
|
||||
RELATIVE_ESCAPE_DENIED.set(null);
|
||||
ABSOLUTE_ESCAPE_DENIED.set(null);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package com.yomahub.liteflow.test.agent.features.workspace;
|
||||
|
||||
import com.yomahub.liteflow.agent.tool.WorkspaceFileTools;
|
||||
import com.yomahub.liteflow.core.FlowExecutor;
|
||||
import com.yomahub.liteflow.flow.LiteflowResponse;
|
||||
import com.yomahub.liteflow.property.LiteflowConfig;
|
||||
import com.yomahub.liteflow.property.agent.ShellMode;
|
||||
import com.yomahub.liteflow.test.agent.features.support.CompatibleCustomEchoAgentComponent;
|
||||
import com.yomahub.liteflow.test.agent.features.support.ReActAgentFeatureTestSupport;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 覆盖 guide 中 workspace 文件工具的关键安全边界。
|
||||
*
|
||||
* <p>测试不让模型自行决定是否调用工具,而是在 Agent 生命周期内直接构造
|
||||
* {@link WorkspaceFileTools},这样可以稳定验证工具行为,同时仍通过 THEN 链路触发。
|
||||
*/
|
||||
@TestPropertySource(value = "classpath:/agent/features/workspace/application.properties")
|
||||
@SpringBootTest(classes = WorkspaceFeatureTest.class)
|
||||
@EnableAutoConfiguration
|
||||
@ComponentScan({ "com.yomahub.liteflow.test.agent.features.workspace.cmp" })
|
||||
public class WorkspaceFeatureTest {
|
||||
|
||||
@Resource
|
||||
private FlowExecutor flowExecutor;
|
||||
|
||||
@Resource
|
||||
private LiteflowConfig liteflowConfig;
|
||||
|
||||
@BeforeEach
|
||||
public void reset() throws Exception {
|
||||
ReActAgentFeatureTestSupport.ensureAgentConfig(
|
||||
liteflowConfig,
|
||||
"target/wk_react_agent_workspace",
|
||||
false,
|
||||
null,
|
||||
ShellMode.DISABLED);
|
||||
liteflowConfig.getAgent().getWorkspace().setMaxFileBytes(4);
|
||||
liteflowConfig.getAgent().getWorkspace().setMaxListSize(1);
|
||||
ReActAgentFeatureTestSupport.resetAgentSessionManager();
|
||||
CompatibleCustomEchoAgentComponent.resetCompatibleProbe();
|
||||
WorkspaceFeatureProbe.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWorkspaceToolsReadWriteListDeleteAndDenyEscapes() {
|
||||
LiteflowResponse response = flowExecutor.execute2Resp("workspaceFeatureChain", "workspace-tools");
|
||||
|
||||
Assertions.assertTrue(response.isSuccess(), response.getMessage());
|
||||
Assertions.assertEquals("abcd", WorkspaceFeatureProbe.TRUNCATED_READ.get(),
|
||||
"read_file 应按 max-file-bytes 截断读取");
|
||||
Assertions.assertEquals(1, WorkspaceFeatureProbe.LIST_RESULT.get().size(),
|
||||
"list_files 应按 max-list-size 限制返回数量");
|
||||
Assertions.assertTrue(WorkspaceFeatureProbe.RELATIVE_ESCAPE_DENIED.get().contains("path escapes workspace"));
|
||||
Assertions.assertTrue(WorkspaceFeatureProbe.ABSOLUTE_ESCAPE_DENIED.get().contains("absolute path denied"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.yomahub.liteflow.test.agent.features.workspace.cmp;
|
||||
|
||||
import com.yomahub.liteflow.agent.tool.WorkspaceFileTools;
|
||||
import com.yomahub.liteflow.test.agent.features.support.CompatibleCustomEchoAgentComponent;
|
||||
import com.yomahub.liteflow.test.agent.features.workspace.WorkspaceFeatureProbe;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 验证 WorkspaceFileTools 的具体行为,并将检查结果暴露给测试断言。
|
||||
*/
|
||||
@Component("workspaceAgent")
|
||||
public class WorkspaceAgentCmp extends CompatibleCustomEchoAgentComponent {
|
||||
@Override
|
||||
protected String userPrompt() {
|
||||
WorkspaceFileTools tools = new WorkspaceFileTools(ctx().getWorkspaceDir(), agentConfig());
|
||||
tools.writeFile("notes/a.txt", "abcdef");
|
||||
tools.writeFile("notes/b.txt", "ghijkl");
|
||||
WorkspaceFeatureProbe.TRUNCATED_READ.set(tools.readFile("notes/a.txt"));
|
||||
WorkspaceFeatureProbe.LIST_RESULT.set(tools.listFiles("notes"));
|
||||
tools.deleteFile("notes/b.txt");
|
||||
try {
|
||||
tools.readFile("../escape.txt");
|
||||
} catch (SecurityException e) {
|
||||
WorkspaceFeatureProbe.RELATIVE_ESCAPE_DENIED.set(e.getMessage());
|
||||
}
|
||||
try {
|
||||
tools.readFile("/tmp/escape.txt");
|
||||
} catch (SecurityException e) {
|
||||
WorkspaceFeatureProbe.ABSOLUTE_ESCAPE_DENIED.set(e.getMessage());
|
||||
}
|
||||
return super.userPrompt();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.yomahub.liteflow.test.agent.features.workspace.cmp;
|
||||
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* workspace 功能包的准备节点。
|
||||
*/
|
||||
@Component("workspacePrepare")
|
||||
public class WorkspacePrepareCmp extends NodeComponent {
|
||||
@Override
|
||||
public void process() {
|
||||
getSlot().setChainReqData(getSlot().getChainId(), getRequestData());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.yomahub.liteflow.test.agent.features.workspace.cmp;
|
||||
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* workspace 功能包的记录节点。
|
||||
*/
|
||||
@Component("workspaceRecord")
|
||||
public class WorkspaceRecordCmp extends NodeComponent {
|
||||
@Override
|
||||
public void process() {
|
||||
getSlot().setOutput(getNodeId(), getSlot().getResponseData());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
liteflow.rule-source=agent/features/compatiblecustom/flow.el.xml
|
||||
liteflow.print-banner=false
|
||||
|
||||
# 本功能包验证 OpenAI compatible-custom 配置路径。这里使用假 key 和本地 baseUrl,
|
||||
# 组件会解析 ModelSpec 但不会发起真实网络请求。
|
||||
liteflow.agent.workspace.root=target/wk_react_agent_compatiblecustom
|
||||
liteflow.agent.shell.mode=disabled
|
||||
liteflow.agent.defaults.max-iterations=6
|
||||
liteflow.agent.logging.react-enabled=false
|
||||
liteflow.agent.openai-compatible.compatible-custom.api-key=test-compatible-key
|
||||
liteflow.agent.openai-compatible.compatible-custom.base-url=http://127.0.0.1:65535/v1
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE flow PUBLIC "liteflow" "liteflow.dtd">
|
||||
<flow>
|
||||
|
||||
<!-- compatible-custom 功能包:EL 统一使用 THEN 覆盖 LiteFlow 整体编排。 -->
|
||||
<chain name="compatibleCustomFeatureChain">
|
||||
THEN(compatibleCustomPrepare, compatibleCustomAgent, compatibleCustomRecord);
|
||||
</chain>
|
||||
|
||||
</flow>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
liteflow.rule-source=agent/features/conversation/flow.el.xml
|
||||
liteflow.print-banner=false
|
||||
|
||||
# conversation 功能包验证 conversationId 与 agentKey 的拆分语义。
|
||||
# 仍使用 compatible-custom 的假凭据,仅覆盖配置解析路径,不进行真实模型请求。
|
||||
liteflow.agent.workspace.root=target/wk_react_agent_conversation
|
||||
liteflow.agent.shell.mode=disabled
|
||||
liteflow.agent.defaults.max-iterations=6
|
||||
liteflow.agent.logging.react-enabled=false
|
||||
liteflow.agent.openai-compatible.compatible-custom.api-key=test-compatible-key
|
||||
liteflow.agent.openai-compatible.compatible-custom.base-url=http://127.0.0.1:65535/v1
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE flow PUBLIC "liteflow" "liteflow.dtd">
|
||||
<flow>
|
||||
|
||||
<!-- conversation 功能包:两个 Agent 串在 THEN 中,验证同一 conversation 共享 workspace。 -->
|
||||
<chain name="conversationFeatureChain">
|
||||
THEN(conversationPrepare, conversationAgentA, conversationAgentB, conversationRecord);
|
||||
</chain>
|
||||
|
||||
</flow>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
liteflow.rule-source=agent/features/platform/flow.el.xml
|
||||
liteflow.print-banner=false
|
||||
|
||||
# platform 功能包是真实 OpenAI compatible-custom 端点的冒烟测试。
|
||||
# 默认不配置 key/baseUrl,测试会跳过;测试者需要显式提供下面两个环境变量。
|
||||
liteflow.agent.workspace.root=target/wk_react_agent_platform
|
||||
liteflow.agent.shell.mode=disabled
|
||||
liteflow.agent.defaults.max-iterations=3
|
||||
liteflow.agent.logging.react-enabled=false
|
||||
liteflow.agent.openai-compatible.compatible-custom.api-key=${TEST_LITEFLOW_COMPATIBLE_CUSTOM_API_KEY:}
|
||||
liteflow.agent.openai-compatible.compatible-custom.base-url=${TEST_LITEFLOW_COMPATIBLE_CUSTOM_BASE_URL:}
|
||||
test.compatible-custom.model=${TEST_LITEFLOW_COMPATIBLE_CUSTOM_MODEL:gpt-4o-mini}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE flow PUBLIC "liteflow" "liteflow.dtd">
|
||||
<flow>
|
||||
|
||||
<!-- platform 功能包:真实端点冒烟测试也统一通过 THEN 链路触发。 -->
|
||||
<chain name="compatibleCustomPlatformChain">
|
||||
THEN(platformPrepare, compatibleCustomLiveAgent, platformRecord);
|
||||
</chain>
|
||||
|
||||
</flow>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
liteflow.rule-source=agent/features/shell/flow.el.xml
|
||||
liteflow.print-banner=false
|
||||
|
||||
# shell 功能包验证受管 Shell 工具的工作目录和白名单策略。
|
||||
liteflow.agent.workspace.root=target/wk_react_agent_shell
|
||||
liteflow.agent.shell.mode=whitelist
|
||||
liteflow.agent.shell.whitelist=pwd
|
||||
liteflow.agent.shell.max-output-bytes=2048
|
||||
liteflow.agent.defaults.max-iterations=6
|
||||
liteflow.agent.logging.react-enabled=false
|
||||
liteflow.agent.openai-compatible.compatible-custom.api-key=test-compatible-key
|
||||
liteflow.agent.openai-compatible.compatible-custom.base-url=http://127.0.0.1:65535/v1
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE flow PUBLIC "liteflow" "liteflow.dtd">
|
||||
<flow>
|
||||
|
||||
<!-- shell 功能包:在 THEN 链路中验证受管 Shell 工具,不使用 IF/WHEN。 -->
|
||||
<chain name="shellFeatureChain">
|
||||
THEN(shellPrepare, shellAgent, shellRecord);
|
||||
</chain>
|
||||
|
||||
</flow>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
liteflow.rule-source=agent/features/skills/flow.el.xml
|
||||
liteflow.print-banner=false
|
||||
|
||||
# skills 功能包验证 SkillBox 加载和 usedSkills() 记录。compatible-custom 使用假凭据,
|
||||
# 模型调用由本地测试桩完成。
|
||||
liteflow.agent.workspace.root=target/wk_react_agent_skills
|
||||
liteflow.agent.shell.mode=disabled
|
||||
liteflow.agent.defaults.max-iterations=6
|
||||
liteflow.agent.logging.react-enabled=false
|
||||
liteflow.agent.skills.enabled=true
|
||||
liteflow.agent.skills.path=src/test/resources/agent/features/skills
|
||||
liteflow.agent.skills.strict=true
|
||||
liteflow.agent.openai-compatible.compatible-custom.api-key=test-compatible-key
|
||||
liteflow.agent.openai-compatible.compatible-custom.base-url=http://127.0.0.1:65535/v1
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: feature-demo
|
||||
description: Feature demo skill for LiteFlow ReAct Agent tests
|
||||
---
|
||||
|
||||
# Feature Demo Skill
|
||||
|
||||
Use this skill when a test request asks the agent to load the feature demo skill.
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE flow PUBLIC "liteflow" "liteflow.dtd">
|
||||
<flow>
|
||||
|
||||
<!-- skills 功能包:使用 THEN 链路验证 load_skill_through_path 与 usedSkills()。 -->
|
||||
<chain name="skillsFeatureChain">
|
||||
THEN(skillsPrepare, skillsAgent, skillsRecord);
|
||||
</chain>
|
||||
|
||||
</flow>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
liteflow.rule-source=agent/features/workspace/flow.el.xml
|
||||
liteflow.print-banner=false
|
||||
|
||||
# workspace 功能包验证内置文件工具的真实读写、截断、列表上限和路径越界保护。
|
||||
liteflow.agent.workspace.root=target/wk_react_agent_workspace
|
||||
liteflow.agent.workspace.max-file-bytes=4
|
||||
liteflow.agent.workspace.max-list-size=1
|
||||
liteflow.agent.shell.mode=disabled
|
||||
liteflow.agent.defaults.max-iterations=6
|
||||
liteflow.agent.logging.react-enabled=false
|
||||
liteflow.agent.openai-compatible.compatible-custom.api-key=test-compatible-key
|
||||
liteflow.agent.openai-compatible.compatible-custom.base-url=http://127.0.0.1:65535/v1
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE flow PUBLIC "liteflow" "liteflow.dtd">
|
||||
<flow>
|
||||
|
||||
<!-- workspace 功能包:文件工具在 Agent process 生命周期内执行,整链仍由 THEN 驱动。 -->
|
||||
<chain name="workspaceFeatureChain">
|
||||
THEN(workspacePrepare, workspaceAgent, workspaceRecord);
|
||||
</chain>
|
||||
|
||||
</flow>
|
||||
Loading…
Reference in New Issue