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:
everywhere.z 2026-05-13 16:24:21 +08:00
parent 65a6213b72
commit 2011d6898b
46 changed files with 1250 additions and 0 deletions

View File

@ -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"));
}
}

View File

@ -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 {
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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 conversationIdagentKey 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());
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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"));
}
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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<>());
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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";
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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";
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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"));
}
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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}

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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.

View File

@ -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>

View File

@ -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

View File

@ -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>