groupTools = group.getTools();
-
- // Remove group mapping from tool index
- for (String toolName : groupTools) {
- if (removeGroupFromToolIndex(toolName, groupName)) {
- toolsToRemove.add(toolName);
- }
- }
-
- // Remove from active groups
- activeGroups.remove(groupName);
-
- logger.info(
- "Removed tool group '{}' with {} tools", groupName, group.getTools().size());
- }
-
- return toolsToRemove;
- }
-
- /**
- * Get notes about activated tool groups for display to user/agent.
- *
- * @return Formatted string describing active tool groups
- */
- public String getActivatedNotes() {
- if (activeGroups.isEmpty()) {
- return "No tool groups are currently activated.";
- }
-
- StringBuilder notes = new StringBuilder("Activated tool groups:\n");
- for (String groupName : activeGroups) {
- ToolGroup group = toolGroups.get(groupName);
- if (group != null) {
- notes.append(String.format("- %s: %s\n", groupName, group.getDescription()));
- }
- }
- return notes.toString();
- }
-
- /**
- * Get notes about all tool groups for display to user/agent.
- *
- * @return Formatted string describing active tool groups
- */
- public String getNotes() {
- StringBuilder activatedNotes = new StringBuilder("Activated tool groups:\n");
- StringBuilder inactiveNotes = new StringBuilder("Inactive tool groups:\n");
- boolean hasActivatedGroup = false;
- boolean hasInactiveGroup = false;
- for (ToolGroup group : toolGroups.values()) {
- if (group.isActive()) {
- hasActivatedGroup = true;
- activatedNotes.append(
- String.format("- %s: %s\n", group.getName(), group.getDescription()));
- } else {
- hasInactiveGroup = true;
- inactiveNotes.append(
- String.format("- %s: %s\n", group.getName(), group.getDescription()));
- }
- }
-
- if (!hasActivatedGroup) {
- activatedNotes.append("No tool groups are currently activated.\n");
- }
-
- if (!hasInactiveGroup) {
- inactiveNotes.append("No tool groups are currently inactive.\n");
- }
-
- activatedNotes.append(inactiveNotes);
- return activatedNotes.toString();
- }
-
- /**
- * Validate that a group exists.
- *
- * @param groupName Group name to validate
- * @throws IllegalArgumentException if group doesn't exist
- */
- public void validateGroupExists(String groupName) {
- if (!toolGroups.containsKey(groupName)) {
- throw new IllegalArgumentException(
- String.format("Tool group '%s' does not exist", groupName));
- }
- }
-
- /**
- * Check if a group is active.
- *
- * @param groupName Group name
- * @return true if the group exists and is active
- */
- public boolean isActiveGroup(String groupName) {
- if (groupName == null) {
- return false;
- }
- ToolGroup group = toolGroups.get(groupName);
- return group != null && group.isActive();
- }
-
- /**
- * Check if a tool is in any active group.
- *
- * If the tool is not in any group, it is considered active by default.
- *
- * @param toolName Tool name
- * @return true if ungrouped or in at least one active group
- */
- public boolean isActiveTool(String toolName) {
- if (toolName == null) {
- return false;
- }
- Set groups = tools.get(toolName);
- if (groups == null || groups.isEmpty()) {
- return true;
- }
- for (String groupName : groups) {
- if (isActiveGroup(groupName)) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Check whether a tool belongs to any group.
- *
- * @param toolName Tool name
- * @return true if the tool is in at least one group
- */
- public boolean isGroupedTool(String toolName) {
- if (toolName == null) {
- return false;
- }
- Set groups = tools.get(toolName);
- return groups != null && !groups.isEmpty();
- }
-
- /**
- * Get all tools that belong to active groups.
- *
- * @return Set of tool names that are in active groups
- */
- public Set getActiveToolNames() {
- Set activeTools = new HashSet<>();
- for (ToolGroup group : toolGroups.values()) {
- if (group.isActive()) {
- activeTools.addAll(group.getTools());
- }
- }
- return activeTools;
- }
-
- /**
- * Add a tool to a group.
- *
- * @param groupName Group name
- * @param toolName Tool name
- */
- public void addToolToGroup(String groupName, String toolName) {
- ToolGroup group = toolGroups.get(groupName);
- if (group != null) {
- group.addTool(toolName);
- tools.computeIfAbsent(toolName, key -> ConcurrentHashMap.newKeySet()).add(groupName);
- }
- }
-
- /**
- * Remove a tool from a group.
- *
- * @param groupName Group name
- * @param toolName Tool name
- */
- public void removeToolFromGroup(String groupName, String toolName) {
- ToolGroup group = toolGroups.get(groupName);
- if (group != null) {
- group.removeTool(toolName);
- removeGroupFromToolIndex(toolName, groupName);
- }
- }
-
- /**
- * Get all tool group names.
- *
- * @return Set of all tool group names
- */
- public Set getToolGroupNames() {
- return new HashSet<>(toolGroups.keySet());
- }
-
- /**
- * Get active tool group names.
- *
- * @return List of active group names
- */
- public List getActiveGroups() {
- return new ArrayList<>(activeGroups);
- }
-
- /**
- * Set active groups (for state restoration).
- *
- * @param activeGroups List of group names to mark as active
- */
- public void setActiveGroups(List activeGroups) {
- this.activeGroups = new ArrayList<>(activeGroups);
-
- // Mark corresponding groups as active
- for (String groupName : activeGroups) {
- ToolGroup group = toolGroups.get(groupName);
- if (group != null) {
- group.setActive(true);
- }
- }
- }
-
- /**
- * Get a tool group by name.
- *
- * @param groupName Name of the tool group
- * @return ToolGroup or null if not found
- */
- public ToolGroup getToolGroup(String groupName) {
- return toolGroups.get(groupName);
- }
-
- /**
- * Copy all tool groups from this manager to another manager.
- *
- * @param target The target manager to copy tool groups to
- */
- void copyTo(ToolGroupManager target) {
- target.tools.clear();
- target.toolGroups.clear();
- for (Map.Entry entry : toolGroups.entrySet()) {
- String groupName = entry.getKey();
- ToolGroup sourceGroup = entry.getValue();
-
- // Create a copy of the tool group
- ToolGroup copiedGroup =
- ToolGroup.builder()
- .name(groupName)
- .description(sourceGroup.getDescription())
- .active(sourceGroup.isActive())
- .tools(sourceGroup.getTools())
- .build();
-
- target.toolGroups.put(groupName, copiedGroup);
- }
-
- for (Map.Entry> entry : tools.entrySet()) {
- target.tools.put(entry.getKey(), new HashSet<>(entry.getValue()));
- }
-
- // Copy activeGroups list
- target.activeGroups = new ArrayList<>(this.activeGroups);
- }
-
- private boolean removeGroupFromToolIndex(String toolName, String groupName) {
- Set groupNames = tools.get(toolName);
- if (groupNames == null) {
- return false;
- }
- groupNames.remove(groupName);
- if (groupNames.isEmpty()) {
- tools.remove(toolName);
- return true;
- }
- return false;
- }
-}
+/*
+ * 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.tool;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArraySet;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Manages tool groups and their activation states.
+ */
+class ToolGroupManager {
+
+ private static final Logger logger = LoggerFactory.getLogger(ToolGroupManager.class);
+
+ private final Map toolGroups = new ConcurrentHashMap<>(); // group -> tools
+ private final Map> tools = new ConcurrentHashMap<>(); // tool -> groups
+ private final Set activeGroups = new CopyOnWriteArraySet<>();
+
+ /**
+ * Create tool groups and record them in the manager.
+ *
+ * @param groupName Name of the tool group
+ * @param description Description of the tool group for the agent to understand
+ * @param active Whether the tool group is active by default
+ * @throws IllegalArgumentException if group already exists
+ */
+ public void createToolGroup(String groupName, String description, boolean active) {
+ if (toolGroups.containsKey(groupName)) {
+ throw new IllegalArgumentException(
+ String.format("Tool group '%s' already exists", groupName));
+ }
+
+ ToolGroup group =
+ ToolGroup.builder().name(groupName).description(description).active(active).build();
+
+ toolGroups.put(groupName, group);
+
+ if (active) {
+ setGroupActiveState(groupName, group, true);
+ }
+
+ logger.info("Created tool group '{}': {}", groupName, description);
+ }
+
+ /**
+ * Creates a tool group with default active status (true).
+ *
+ * @param groupName Name of the tool group
+ * @param description Description of the tool group for the agent to understand
+ * @throws IllegalArgumentException if group already exists
+ */
+ public void createToolGroup(String groupName, String description) {
+ createToolGroup(groupName, description, true);
+ }
+
+ /**
+ * Update the active status of tool groups.
+ *
+ * @param groupNames List of tool group names to update
+ * @param active Whether to activate or deactivate
+ * @throws IllegalArgumentException if any group doesn't exist
+ */
+ public void updateToolGroups(List groupNames, boolean active) {
+ for (String groupName : groupNames) {
+ ToolGroup group = toolGroups.get(groupName);
+ if (group == null) {
+ throw new IllegalArgumentException(
+ String.format("Tool group '%s' does not exist", groupName));
+ }
+ setGroupActiveState(groupName, group, active);
+
+ logger.info("Tool group '{}' active status set to: {}", groupName, active);
+ }
+ }
+
+ /**
+ * Remove tool groups.
+ * Note: Caller is responsible for removing associated tools.
+ *
+ * @param groupNames List of tool group names to remove
+ * @return Set of tool names that were in the removed groups
+ */
+ public Set removeToolGroups(List groupNames) {
+ Set toolsToRemove = new HashSet<>();
+
+ for (String groupName : groupNames) {
+ ToolGroup group = toolGroups.remove(groupName);
+ if (group == null) {
+ logger.warn("Tool group '{}' does not exist, skipping removal", groupName);
+ continue;
+ }
+
+ // Collect tools from this group
+ Set groupTools = group.getTools();
+
+ // Remove group mapping from tool index
+ for (String toolName : groupTools) {
+ if (removeGroupFromToolIndex(toolName, groupName)) {
+ toolsToRemove.add(toolName);
+ }
+ }
+
+ // Remove from active groups
+ activeGroups.remove(groupName);
+
+ logger.info(
+ "Removed tool group '{}' with {} tools", groupName, group.getTools().size());
+ }
+
+ return toolsToRemove;
+ }
+
+ /**
+ * Get notes about activated tool groups for display to user/agent.
+ *
+ * @return Formatted string describing active tool groups
+ */
+ public String getActivatedNotes() {
+ List activeGroupSnapshot = getActiveGroups();
+ if (activeGroupSnapshot.isEmpty()) {
+ return "No tool groups are currently activated.";
+ }
+
+ StringBuilder notes = new StringBuilder("Activated tool groups:\n");
+ for (String groupName : activeGroupSnapshot) {
+ ToolGroup group = toolGroups.get(groupName);
+ if (group != null) {
+ notes.append(String.format("- %s: %s\n", groupName, group.getDescription()));
+ }
+ }
+ return notes.toString();
+ }
+
+ /**
+ * Get notes about all tool groups for display to user/agent.
+ *
+ * @return Formatted string describing active tool groups
+ */
+ public String getNotes() {
+ StringBuilder activatedNotes = new StringBuilder("Activated tool groups:\n");
+ StringBuilder inactiveNotes = new StringBuilder("Inactive tool groups:\n");
+ boolean hasActivatedGroup = false;
+ boolean hasInactiveGroup = false;
+ for (ToolGroup group : toolGroups.values()) {
+ if (group.isActive()) {
+ hasActivatedGroup = true;
+ activatedNotes.append(
+ String.format("- %s: %s\n", group.getName(), group.getDescription()));
+ } else {
+ hasInactiveGroup = true;
+ inactiveNotes.append(
+ String.format("- %s: %s\n", group.getName(), group.getDescription()));
+ }
+ }
+
+ if (!hasActivatedGroup) {
+ activatedNotes.append("No tool groups are currently activated.\n");
+ }
+
+ if (!hasInactiveGroup) {
+ inactiveNotes.append("No tool groups are currently inactive.\n");
+ }
+
+ activatedNotes.append(inactiveNotes);
+ return activatedNotes.toString();
+ }
+
+ /**
+ * Validate that a group exists.
+ *
+ * @param groupName Group name to validate
+ * @throws IllegalArgumentException if group doesn't exist
+ */
+ public void validateGroupExists(String groupName) {
+ if (!toolGroups.containsKey(groupName)) {
+ throw new IllegalArgumentException(
+ String.format("Tool group '%s' does not exist", groupName));
+ }
+ }
+
+ /**
+ * Check if a group is active.
+ *
+ * @param groupName Group name
+ * @return true if the group exists and is active
+ */
+ public boolean isActiveGroup(String groupName) {
+ if (groupName == null) {
+ return false;
+ }
+ ToolGroup group = toolGroups.get(groupName);
+ return group != null && group.isActive();
+ }
+
+ /**
+ * Check if a tool is in any active group.
+ *
+ * If the tool is not in any group, it is considered active by default.
+ *
+ * @param toolName Tool name
+ * @return true if ungrouped or in at least one active group
+ */
+ public boolean isActiveTool(String toolName) {
+ if (toolName == null) {
+ return false;
+ }
+ Set groups = tools.get(toolName);
+ if (groups == null || groups.isEmpty()) {
+ return true;
+ }
+ for (String groupName : groups) {
+ if (isActiveGroup(groupName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check whether a tool belongs to any group.
+ *
+ * @param toolName Tool name
+ * @return true if the tool is in at least one group
+ */
+ public boolean isGroupedTool(String toolName) {
+ if (toolName == null) {
+ return false;
+ }
+ Set groups = tools.get(toolName);
+ return groups != null && !groups.isEmpty();
+ }
+
+ /**
+ * Get all tools that belong to active groups.
+ *
+ * @return Set of tool names that are in active groups
+ */
+ public Set getActiveToolNames() {
+ Set activeTools = new HashSet<>();
+ for (ToolGroup group : toolGroups.values()) {
+ if (group.isActive()) {
+ activeTools.addAll(group.getTools());
+ }
+ }
+ return activeTools;
+ }
+
+ /**
+ * Add a tool to a group.
+ *
+ * @param groupName Group name
+ * @param toolName Tool name
+ */
+ public void addToolToGroup(String groupName, String toolName) {
+ ToolGroup group = toolGroups.get(groupName);
+ if (group != null) {
+ group.addTool(toolName);
+ tools.computeIfAbsent(toolName, key -> ConcurrentHashMap.newKeySet()).add(groupName);
+ }
+ }
+
+ /**
+ * Remove a tool from a group.
+ *
+ * @param groupName Group name
+ * @param toolName Tool name
+ */
+ public void removeToolFromGroup(String groupName, String toolName) {
+ ToolGroup group = toolGroups.get(groupName);
+ if (group != null) {
+ group.removeTool(toolName);
+ removeGroupFromToolIndex(toolName, groupName);
+ }
+ }
+
+ /**
+ * Get all tool group names.
+ *
+ * @return Set of all tool group names
+ */
+ public Set getToolGroupNames() {
+ return new HashSet<>(toolGroups.keySet());
+ }
+
+ /**
+ * Get active tool group names.
+ *
+ * @return List of active group names
+ */
+ public List getActiveGroups() {
+ return new ArrayList<>(activeGroups);
+ }
+
+ /**
+ * Set active groups (for state restoration).
+ *
+ * @param activeGroups List of group names to mark as active
+ */
+ public void setActiveGroups(List activeGroups) {
+ Set desiredActiveGroups = new LinkedHashSet<>(activeGroups);
+ this.activeGroups.clear();
+ this.activeGroups.addAll(desiredActiveGroups);
+
+ for (ToolGroup group : toolGroups.values()) {
+ group.setActive(desiredActiveGroups.contains(group.getName()));
+ }
+ }
+
+ /**
+ * Get a tool group by name.
+ *
+ * @param groupName Name of the tool group
+ * @return ToolGroup or null if not found
+ */
+ public ToolGroup getToolGroup(String groupName) {
+ return toolGroups.get(groupName);
+ }
+
+ /**
+ * Copy all tool groups from this manager to another manager.
+ *
+ * @param target The target manager to copy tool groups to
+ */
+ void copyTo(ToolGroupManager target) {
+ target.tools.clear();
+ target.toolGroups.clear();
+ for (Map.Entry entry : toolGroups.entrySet()) {
+ String groupName = entry.getKey();
+ ToolGroup sourceGroup = entry.getValue();
+
+ // Create a copy of the tool group
+ ToolGroup copiedGroup =
+ ToolGroup.builder()
+ .name(groupName)
+ .description(sourceGroup.getDescription())
+ .active(sourceGroup.isActive())
+ .tools(sourceGroup.getTools())
+ .build();
+
+ target.toolGroups.put(groupName, copiedGroup);
+ }
+
+ for (Map.Entry> entry : tools.entrySet()) {
+ target.tools.put(entry.getKey(), new HashSet<>(entry.getValue()));
+ }
+
+ target.activeGroups.clear();
+ target.activeGroups.addAll(this.activeGroups);
+ }
+
+ private void setGroupActiveState(String groupName, ToolGroup group, boolean active) {
+ group.setActive(active);
+ if (active) {
+ activeGroups.add(groupName);
+ return;
+ }
+ activeGroups.remove(groupName);
+ }
+
+ private boolean removeGroupFromToolIndex(String toolName, String groupName) {
+ Set groupNames = tools.get(toolName);
+ if (groupNames == null) {
+ return false;
+ }
+ groupNames.remove(groupName);
+ if (groupNames.isEmpty()) {
+ tools.remove(toolName);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/agentscope-core/src/test/java/io/agentscope/core/tool/ToolGroupManagerConcurrencyTest.java b/agentscope-core/src/test/java/io/agentscope/core/tool/ToolGroupManagerConcurrencyTest.java
new file mode 100644
index 000000000..358d6b6b0
--- /dev/null
+++ b/agentscope-core/src/test/java/io/agentscope/core/tool/ToolGroupManagerConcurrencyTest.java
@@ -0,0 +1,203 @@
+/*
+ * 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.tool;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.Test;
+
+class ToolGroupManagerConcurrencyTest {
+
+ private static final int GROUP_COUNT = 10;
+ private static final int WORKER_COUNT = 6;
+ private static final int ITERATIONS = 250;
+
+ @Test
+ void concurrentReadersAndWritersShouldKeepActiveGroupSnapshotsConsistent() throws Exception {
+ ToolGroupManager manager = createManagerWithGroups();
+ Queue failures = new ConcurrentLinkedQueue<>();
+ ExecutorService executor = Executors.newFixedThreadPool(WORKER_COUNT);
+ CyclicBarrier startBarrier = new CyclicBarrier(WORKER_COUNT);
+
+ try {
+ List> futures = new ArrayList<>();
+ for (int worker = 0; worker < WORKER_COUNT / 2; worker++) {
+ final int workerIndex = worker;
+ futures.add(
+ executor.submit(
+ () -> {
+ await(startBarrier, failures);
+ for (int iteration = 0;
+ iteration < ITERATIONS && failures.isEmpty();
+ iteration++) {
+ String groupName =
+ "group-"
+ + ((workerIndex + iteration) % GROUP_COUNT);
+ boolean active = (iteration + workerIndex) % 2 == 0;
+ try {
+ manager.updateToolGroups(List.of(groupName), active);
+ } catch (Throwable error) {
+ failures.add(error);
+ return;
+ }
+ }
+ }));
+ }
+
+ for (int worker = WORKER_COUNT / 2; worker < WORKER_COUNT; worker++) {
+ futures.add(
+ executor.submit(
+ () -> {
+ await(startBarrier, failures);
+ for (int iteration = 0;
+ iteration < ITERATIONS && failures.isEmpty();
+ iteration++) {
+ try {
+ List snapshot = manager.getActiveGroups();
+ assertEquals(
+ new HashSet<>(snapshot).size(),
+ snapshot.size(),
+ "Concurrent snapshots should not contain"
+ + " duplicates");
+ manager.getActivatedNotes();
+ manager.getNotes();
+ manager.getActiveToolNames();
+ } catch (Throwable error) {
+ failures.add(error);
+ return;
+ }
+ }
+ }));
+ }
+
+ for (Future> future : futures) {
+ future.get(30, TimeUnit.SECONDS);
+ }
+ } finally {
+ executor.shutdownNow();
+ }
+
+ assertTrue(failures.isEmpty(), formatFailures(failures));
+ assertManagerConsistency(manager);
+ }
+
+ @Test
+ void setActiveGroupsShouldReplacePreviousActiveStateSnapshot() {
+ ToolGroupManager manager = createManagerWithGroups();
+
+ manager.setActiveGroups(List.of("group-1", "group-3", "missing-group"));
+
+ assertEquals(
+ List.of("group-1", "group-3", "missing-group"),
+ manager.getActiveGroups(),
+ "setActiveGroups should publish a stable snapshot in the provided order");
+ assertTrue(manager.getToolGroup("group-1").isActive());
+ assertTrue(manager.getToolGroup("group-3").isActive());
+ assertFalse(manager.getToolGroup("group-0").isActive());
+ assertFalse(manager.getToolGroup("group-2").isActive());
+ assertFalse(manager.getToolGroup("group-4").isActive());
+
+ String notes = manager.getActivatedNotes();
+ assertTrue(notes.contains("group-1"));
+ assertTrue(notes.contains("group-3"));
+ assertFalse(notes.contains("missing-group"));
+ }
+
+ @Test
+ void copyToShouldPreserveAnIndependentActiveGroupSnapshot() {
+ ToolGroupManager source = createManagerWithGroups();
+ source.setActiveGroups(List.of("group-2", "group-4", "missing-group"));
+
+ ToolGroupManager copy = new ToolGroupManager();
+ source.copyTo(copy);
+
+ assertEquals(source.getToolGroupNames(), copy.getToolGroupNames());
+ assertEquals(source.getActiveGroups(), copy.getActiveGroups());
+ assertManagerConsistency(copy);
+
+ copy.updateToolGroups(List.of("group-2"), false);
+ copy.updateToolGroups(List.of("group-5"), true);
+
+ assertTrue(source.getActiveGroups().contains("group-2"));
+ assertFalse(source.getActiveGroups().contains("group-5"));
+ assertFalse(copy.getActiveGroups().contains("group-2"));
+ assertTrue(copy.getActiveGroups().contains("group-5"));
+ assertManagerConsistency(source);
+ assertManagerConsistency(copy);
+ }
+
+ private ToolGroupManager createManagerWithGroups() {
+ ToolGroupManager manager = new ToolGroupManager();
+ for (int index = 0; index < GROUP_COUNT; index++) {
+ String groupName = "group-" + index;
+ manager.createToolGroup(groupName, "Description for " + groupName, index % 2 == 0);
+ manager.addToolToGroup(groupName, "tool-" + index);
+ }
+ return manager;
+ }
+
+ private void assertManagerConsistency(ToolGroupManager manager) {
+ List activeGroups = manager.getActiveGroups();
+ Set activeGroupSet = new HashSet<>(activeGroups);
+ assertEquals(
+ activeGroupSet.size(),
+ activeGroups.size(),
+ "activeGroups should stay deduplicated");
+
+ for (String groupName : manager.getToolGroupNames()) {
+ ToolGroup group = manager.getToolGroup(groupName);
+ assertEquals(
+ activeGroupSet.contains(groupName),
+ group.isActive(),
+ "ToolGroup active flag should stay aligned with activeGroups snapshots");
+ }
+ }
+
+ private void await(CyclicBarrier startBarrier, Queue failures) {
+ try {
+ startBarrier.await(10, TimeUnit.SECONDS);
+ } catch (Throwable error) {
+ failures.add(error);
+ }
+ }
+
+ private String formatFailures(Queue failures) {
+ StringBuilder builder = new StringBuilder();
+ for (Throwable failure : failures) {
+ if (builder.length() > 0) {
+ builder.append(" | ");
+ }
+ builder.append(failure.getClass().getSimpleName())
+ .append(':')
+ .append(' ')
+ .append(failure.getMessage());
+ }
+ return builder.toString();
+ }
+}