Files
DemonGatanjieu-Anomalous_Mo…/tests/test_mcp_directory.py
T
DemonGatanjieuandClaude Opus 5.5 59916d1ea3 Offer an on-demand tool set: a few tools, and the directory for the rest
A third choice for "Tools for the AI" in the Gates window. With "On demand",
AI apps are offered only action_catalog.CORE plus two tools of
mcp_directory.py:
- CORE: begin_task, get_situation, run_workflow, run_status, report, ask_user,
  get_answers.
- tool_help: the description and arguments of up to three tools named in the
  directory.
- use_tool: calls any tool of the directory.

tools/list drops to 11 tools and about 9,000 characters (daily 22,200, full
40,700). With this set the directory begin_task brings names every tool and
says how to reach the ones not listed.

use_tool has no run of its own. mcp_server._call makes the call the tool it
names before anything else, so the user's gates, the timeline's step, the run
watch and the answer are all that tool's; the timeline never shows use_tool.
An unknown name, the card's own tools or wrong arguments come back as tool
errors that point to tool_help.

The default stays Full. Switching back to Daily or Full in the Gates window
undoes this without a revert, and this commit stands alone if it has to go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 16:11:41 +08:00

101 lines
5.9 KiB
Python

"""The directory of tools by what an AI wants to do (api/mcp_directory.py): built from the catalog,
only the tools offered, in begin_task's answer and in get_situation's while there is no task; and
the lazy set, where tool_help and use_tool reach the tools not offered directly."""
import sys
import unittest
from pathlib import Path
from unittest import mock
PLUGIN = Path(__file__).resolve().parents[1]
sys.path[:0] = [str(PLUGIN), str(PLUGIN.parents[1]), str(PLUGIN / "tests")]
from api import action_catalog, ai_gates, ai_tasks, mcp_apps, mcp_directory, mcp_server # noqa: E402
from test_mcp import rpc # noqa: E402 (its module keeps tasks and gates in temporary folders)
def call(name, arguments):
return rpc("tools/call", {"name": name, "arguments": arguments}, name)["result"]
class DirectoryTests(unittest.TestCase):
def test_every_stage_has_a_heading_and_every_tool_a_line(self):
stages = {stage for stage, _heading in mcp_directory.SECTIONS}
self.assertTrue({entry.stage for entry in action_catalog.TOOLS.values()} <= stages, "a new stage needs a heading")
specs = {name: tool["spec"] for name, tool in mcp_server.TOOLS.items() if name not in mcp_apps.APP_TOOLS}
text = " ".join(mcp_directory.directory(specs).values())
self.assertTrue(all(f"{name} (" in text for name in specs), "every tool is in it")
self.assertIn("set_model (Swap a node's model)", text)
def test_only_the_tools_offered_in_the_catalogs_order(self):
specs = {name: mcp_server.TOOLS[name]["spec"] for name in ("run_status", "run_workflow", "zoom_image")}
found = mcp_directory.directory(specs)
self.assertEqual(found["Run a workflow"], "run_workflow (Run the workflow), run_status (How the run is going)")
self.assertEqual(set(found), {"note", "Run a workflow", "Pictures"})
def test_begin_task_carries_the_directory_of_the_tools_offered(self):
with mock.patch.object(ai_gates, "tool_set", return_value="daily"):
result = call("begin_task", {"title": "目录", "new_task": True})
found = result["structuredContent"]["directory"]
self.assertIn("run_workflow", found["Run a workflow"])
self.assertNotIn("download_model", " ".join(found.values()), "not offered in the daily set")
self.assertIn('"directory"', result["content"][0]["text"], "the text says it too")
self.assertLess(len(result["content"][0]["text"]), 4000, "small enough to stay in the chat")
def test_get_situation_carries_it_only_without_a_task(self):
with mock.patch.object(mcp_server.ai_tasks, "active_task_id", return_value=None), \
mock.patch("api.mcp_situation.ask_page", side_effect=mcp_server.mcp_bridge.PageUnavailable("no page")):
self.assertIn("directory", call("get_situation", {})["structuredContent"])
call("begin_task", {"title": "有任务", "new_task": True})
with mock.patch("api.mcp_situation.ask_page", side_effect=mcp_server.mcp_bridge.PageUnavailable("no page")):
self.assertNotIn("directory", call("get_situation", {})["structuredContent"])
class LazyTests(unittest.TestCase):
def setUp(self):
patcher = mock.patch.object(ai_gates, "tool_set", return_value="lazy")
patcher.start()
self.addCleanup(patcher.stop)
def test_a_few_tools_and_the_way_to_the_rest(self):
names = {tool["name"] for tool in rpc("tools/list")["result"]["tools"]}
self.assertEqual(names, action_catalog.CORE | set(mcp_directory.DIRECTORY_TOOLS) | set(mcp_apps.APP_TOOLS))
found = call("begin_task", {"title": "按需", "new_task": True})["structuredContent"]["directory"]
self.assertIn("set_model (Swap a node's model)", found["Models"], "the directory names the tools not offered")
self.assertIn("use_tool(name, arguments)", found["note"])
self.assertNotIn("tool_help (", " ".join(found.values()))
def test_other_sets_do_not_offer_the_way_in(self):
for chosen in ("full", "daily"):
with mock.patch.object(ai_gates, "tool_set", return_value=chosen):
names = {tool["name"] for tool in rpc("tools/list")["result"]["tools"]}
self.assertFalse(names & set(mcp_directory.DIRECTORY_TOOLS), chosen)
def test_tool_help_tells_what_a_tool_takes(self):
answer = call("tool_help", {"names": ["set_model", "nope"]})["structuredContent"]
self.assertEqual(answer["tools"][0]["name"], "set_model")
self.assertIn("model_id", answer["tools"][0]["arguments"])
self.assertEqual(answer["not_found"], ["nope"])
self.assertTrue(call("tool_help", {"names": ["a", "b", "c", "d"]})["isError"], "three at most")
def test_use_tool_is_the_call_of_the_tool_it_names(self):
call("begin_task", {"title": "调用", "new_task": True})
with mock.patch.object(ai_gates, "check", wraps=ai_gates.check) as gate:
result = call("use_tool", {"name": "report", "arguments": {"text": "看看这张"}})
self.assertFalse(result["isError"], result)
self.assertEqual(gate.call_args[0][0], "report", "the user's gates see the tool itself")
task = next(item for item in ai_tasks.list_tasks() if item["title"] == "调用")
tools = [step["tool"] for step in ai_tasks.get_task(task["id"])["steps"]]
self.assertIn("report", tools)
self.assertNotIn("use_tool", tools, "the timeline shows the tool, never use_tool")
def test_a_wrong_use_says_where_to_look(self):
self.assertIn("No tool nope", call("use_tool", {"name": "nope"})["content"][0]["text"])
self.assertTrue(call("use_tool", {"name": "app_answer", "arguments": {}})["isError"], "never the card's own")
failed = call("use_tool", {"name": "report", "arguments": {"words": "x"}})
self.assertTrue(failed["isError"])
self.assertIn('tool_help(names=["report"])', failed["content"][0]["text"])
if __name__ == "__main__":
unittest.main()