diff --git a/agentscope-core/src/test/java/io/agentscope/core/skill/SkillLoadToolFreshSessionTest.java b/agentscope-core/src/test/java/io/agentscope/core/skill/SkillLoadToolFreshSessionTest.java new file mode 100644 index 0000000000..257cb5768b --- /dev/null +++ b/agentscope-core/src/test/java/io/agentscope/core/skill/SkillLoadToolFreshSessionTest.java @@ -0,0 +1,232 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.core.skill; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.agentscope.core.ReActAgent; +import io.agentscope.core.agent.RuntimeContext; +import io.agentscope.core.message.ContentBlock; +import io.agentscope.core.message.Msg; +import io.agentscope.core.message.MsgRole; +import io.agentscope.core.message.TextBlock; +import io.agentscope.core.message.ToolResultBlock; +import io.agentscope.core.message.ToolUseBlock; +import io.agentscope.core.model.ChatModelBase; +import io.agentscope.core.model.ChatResponse; +import io.agentscope.core.model.GenerateOptions; +import io.agentscope.core.model.ToolSchema; +import io.agentscope.core.permission.PermissionContextState; +import io.agentscope.core.permission.PermissionMode; +import io.agentscope.core.tool.Toolkit; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +/** + * Regression tests for #2169. + * + *
Bug: on the first call of a fresh session, {@code activateSlotForContext} applied the
+ * state's (empty) activated group list via {@code setActiveGroups}, deactivating the build-time
+ * active {@code skill-build-in-tools} group. {@code SkillHook} kept advertising skills in the
+ * system prompt, so the model inevitably called {@code load_skill_through_path} and received
+ * {@code Unauthorized tool call: 'load_skill_through_path' is not available}. Fixed on main by
+ * seeding fresh-session tool context from the build-time active groups; these tests pin the
+ * end-to-end behavior through a real {@link SkillBox} + {@link ReActAgent} call.
+ */
+@DisplayName("#2169: skill load tool on fresh sessions")
+class SkillLoadToolFreshSessionTest {
+
+ private static final Duration TIMEOUT = Duration.ofSeconds(15);
+ private static final String SKILL_LOAD_TOOL = "load_skill_through_path";
+ private static final String SKILL_GROUP = "skill-build-in-tools";
+ private static final String SKILL_ID = "demo_skill";
+ private static final String SKILL_BODY = "# Demo Skill Body";
+
+ /** The skill under test, shared so its registered id can be derived rather than hard-coded. */
+ private static final AgentSkill DEMO_SKILL =
+ new AgentSkill(SKILL_ID, "Demo Skill", SKILL_BODY, new HashMap<>());
+
+ /**
+ * The id the load tool actually accepts. {@link AgentSkill#getSkillId()} composes it as
+ * {@code name + "_" + source}, and the 4-arg constructor above defaults {@code source} to
+ * {@code "custom"}, so this resolves to {@code demo_skill_custom} rather than the raw name.
+ * Derived here instead of written literally so the test follows any change to that rule.
+ */
+ private static final String REGISTERED_SKILL_ID = DEMO_SKILL.getSkillId();
+
+ /**
+ * First turn emits a {@code load_skill_through_path} call; on the second turn it records the
+ * tool result exactly as the model would see it, then answers with plain text to end the
+ * loop.
+ */
+ private static final class ScriptModel extends ChatModelBase {
+ private final AtomicInteger turns = new AtomicInteger();
+ private volatile String observedToolResult;
+
+ void reset() {
+ turns.set(0);
+ observedToolResult = null;
+ }
+
+ @Override
+ public String getModelName() {
+ return "script";
+ }
+
+ @Override
+ protected Flux