diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/compatiblecustom/CompatibleCustomFeatureTest.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/compatiblecustom/CompatibleCustomFeatureTest.java new file mode 100644 index 000000000..2960d707a --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/compatiblecustom/CompatibleCustomFeatureTest.java @@ -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 兼容厂商”的基础接入方式。 + * + *
测试链路固定为 {@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")); + } +} diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/compatiblecustom/cmp/CompatibleCustomAgentCmp.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/compatiblecustom/cmp/CompatibleCustomAgentCmp.java new file mode 100644 index 000000000..d646f6fc8 --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/compatiblecustom/cmp/CompatibleCustomAgentCmp.java @@ -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 { +} diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/compatiblecustom/cmp/CompatibleCustomPrepareCmp.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/compatiblecustom/cmp/CompatibleCustomPrepareCmp.java new file mode 100644 index 000000000..bcb6ea556 --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/compatiblecustom/cmp/CompatibleCustomPrepareCmp.java @@ -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 功能包的准备节点。 + * + *
把 LiteFlow 入参写入 chainReqData,供 Agent 组件的 userPrompt() 读取。
+ */
+@Component("compatibleCustomPrepare")
+public class CompatibleCustomPrepareCmp extends NodeComponent {
+ @Override
+ public void process() {
+ getSlot().setChainReqData(getSlot().getChainId(), getRequestData());
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/compatiblecustom/cmp/CompatibleCustomRecordCmp.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/compatiblecustom/cmp/CompatibleCustomRecordCmp.java
new file mode 100644
index 000000000..74f73f209
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/compatiblecustom/cmp/CompatibleCustomRecordCmp.java
@@ -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());
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/conversation/ConversationFeatureProbe.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/conversation/ConversationFeatureProbe.java
new file mode 100644
index 000000000..7f655c85b
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/conversation/ConversationFeatureProbe.java
@@ -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 链路使用 {@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());
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/conversation/cmp/ConversationAgentACmp.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/conversation/cmp/ConversationAgentACmp.java
new file mode 100644
index 000000000..9eb4270c0
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/conversation/cmp/ConversationAgentACmp.java
@@ -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();
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/conversation/cmp/ConversationAgentBCmp.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/conversation/cmp/ConversationAgentBCmp.java
new file mode 100644
index 000000000..40ff983b0
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/conversation/cmp/ConversationAgentBCmp.java
@@ -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();
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/conversation/cmp/ConversationPrepareCmp.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/conversation/cmp/ConversationPrepareCmp.java
new file mode 100644
index 000000000..4b7570e7e
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/conversation/cmp/ConversationPrepareCmp.java
@@ -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());
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/conversation/cmp/ConversationRecordCmp.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/conversation/cmp/ConversationRecordCmp.java
new file mode 100644
index 000000000..025effeea
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/conversation/cmp/ConversationRecordCmp.java
@@ -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());
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/platform/CompatibleCustomPlatformConnectivityTest.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/platform/CompatibleCustomPlatformConnectivityTest.java
new file mode 100644
index 000000000..4270a5101
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/platform/CompatibleCustomPlatformConnectivityTest.java
@@ -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 兼容平台”的真实冒烟场景。
+ *
+ * 该测试默认跳过。运行者需要提供:
+ * {@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);
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/platform/cmp/CompatibleCustomLiveAgentCmp.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/platform/cmp/CompatibleCustomLiveAgentCmp.java
new file mode 100644
index 000000000..b1e454b21
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/platform/cmp/CompatibleCustomLiveAgentCmp.java
@@ -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;
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/platform/cmp/PlatformPrepareCmp.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/platform/cmp/PlatformPrepareCmp.java
new file mode 100644
index 000000000..fbc715ccf
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/platform/cmp/PlatformPrepareCmp.java
@@ -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());
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/platform/cmp/PlatformRecordCmp.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/platform/cmp/PlatformRecordCmp.java
new file mode 100644
index 000000000..546041e50
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/platform/cmp/PlatformRecordCmp.java
@@ -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());
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/shell/ShellFeatureProbe.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/shell/ShellFeatureProbe.java
new file mode 100644
index 000000000..b397c14b3
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/shell/ShellFeatureProbe.java
@@ -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 配置为 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"));
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/shell/cmp/ShellAgentCmp.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/shell/cmp/ShellAgentCmp.java
new file mode 100644
index 000000000..6a53fc265
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/shell/cmp/ShellAgentCmp.java
@@ -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();
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/shell/cmp/ShellPrepareCmp.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/shell/cmp/ShellPrepareCmp.java
new file mode 100644
index 000000000..da1ac1d51
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/shell/cmp/ShellPrepareCmp.java
@@ -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());
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/shell/cmp/ShellRecordCmp.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/shell/cmp/ShellRecordCmp.java
new file mode 100644
index 000000000..3bfa6b003
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/shell/cmp/ShellRecordCmp.java
@@ -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());
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/skills/SkillsFeatureProbe.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/skills/SkillsFeatureProbe.java
new file mode 100644
index 000000000..d7ff9e361
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/skills/SkillsFeatureProbe.java
@@ -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 本测试通过本地模型桩返回一次 {@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());
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/skills/cmp/SkillsAgentCmp.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/skills/cmp/SkillsAgentCmp.java
new file mode 100644
index 000000000..aa7477ddf
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/skills/cmp/SkillsAgentCmp.java
@@ -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 组件仍然通过 {@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;
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/support/CompatibleCustomEchoModel.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/support/CompatibleCustomEchoModel.java
new file mode 100644
index 000000000..e7d53be81
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/support/CompatibleCustomEchoModel.java
@@ -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 这些测试每个功能包都使用独立的 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);
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/workspace/WorkspaceFeatureProbe.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/workspace/WorkspaceFeatureProbe.java
new file mode 100644
index 000000000..872fb9834
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/workspace/WorkspaceFeatureProbe.java
@@ -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 测试不让模型自行决定是否调用工具,而是在 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"));
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/workspace/cmp/WorkspaceAgentCmp.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/workspace/cmp/WorkspaceAgentCmp.java
new file mode 100644
index 000000000..f94574afb
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/workspace/cmp/WorkspaceAgentCmp.java
@@ -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();
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/workspace/cmp/WorkspacePrepareCmp.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/workspace/cmp/WorkspacePrepareCmp.java
new file mode 100644
index 000000000..bffbc3631
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/workspace/cmp/WorkspacePrepareCmp.java
@@ -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());
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/workspace/cmp/WorkspaceRecordCmp.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/workspace/cmp/WorkspaceRecordCmp.java
new file mode 100644
index 000000000..8797094ba
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/workspace/cmp/WorkspaceRecordCmp.java
@@ -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());
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/agent/features/compatiblecustom/application.properties b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/agent/features/compatiblecustom/application.properties
new file mode 100644
index 000000000..8eb51e62f
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/agent/features/compatiblecustom/application.properties
@@ -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
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/agent/features/compatiblecustom/flow.el.xml b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/agent/features/compatiblecustom/flow.el.xml
new file mode 100644
index 000000000..92d51dfce
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/agent/features/compatiblecustom/flow.el.xml
@@ -0,0 +1,10 @@
+
+
+> USED_SKILLS = new AtomicReference<>(List.of());
+ public static final AtomicReference
> TOOL_NAMES = new AtomicReference<>(List.of());
+
+ private SkillsFeatureProbe() {
+ }
+
+ public static void reset() {
+ USED_SKILLS.set(List.of());
+ TOOL_NAMES.set(new CopyOnWriteArrayList<>());
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/skills/SkillsFeatureTest.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/skills/SkillsFeatureTest.java
new file mode 100644
index 000000000..f2918e982
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/features/skills/SkillsFeatureTest.java
@@ -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() 的记录语义。
+ *
+ *
> LIST_RESULT = new AtomicReference<>(List.of());
+ public static final AtomicReference