Files
plyngent/tests/test_tools/test_catalog.py
T
NCBM e9ee71e853 core/tools: show images instead of describing them
New `view_image` reads a local picture (JPEG, PNG, GIF, WebP, sniffed from its
bytes) and hands it to the model as an image part, with an optional `detail`
level — `low` measured 3.4x cheaper on a 1200x900 picture. `read_file` now
points at it instead of returning decode noise.

`fetch_local` returns an image response as the picture itself, and MCP image
content blocks become image parts instead of `[image: ...]` placeholders, so a
screenshot server finally reaches the model.
2026-10-21 18:43:29 +08:00

155 lines
4.9 KiB
Python

"""Tool catalog, tags, and default select parity."""
from __future__ import annotations
import inspect
from plyngent.agent import ToolRegistry, ToolTag, tool
from plyngent.tools import default_tool_definitions, register_builtin_tools
from plyngent.tools.access import ACCESS_TOOLS
from plyngent.tools.catalog import ToolCatalog, ToolSource, catalog_scope, get_catalog, registration_source
from plyngent.tools.chat import CHAT_TOOLS
from plyngent.tools.file import FILE_TOOLS
from plyngent.tools.net import NET_TOOLS
from plyngent.tools.process import PROCESS_TOOLS
from plyngent.tools.skills import SKILL_TOOLS
from plyngent.tools.todo import TODO_TOOLS
from plyngent.tools.vcs import VCS_TOOLS
def test_default_tool_names_match_group_lists() -> None:
register_builtin_tools()
selected = default_tool_definitions(surface="local")
groups = [
*ACCESS_TOOLS,
*FILE_TOOLS,
*PROCESS_TOOLS,
*VCS_TOOLS,
*CHAT_TOOLS,
*TODO_TOOLS,
*SKILL_TOOLS,
*NET_TOOLS,
]
assert sorted(t.name for t in selected) == sorted(t.name for t in groups)
assert len(selected) == len(groups)
def test_all_builtins_are_async_and_tagged() -> None:
tools = default_tool_definitions()
assert tools
for definition in tools:
assert inspect.iscoroutinefunction(definition.handler), definition.name
assert definition.tags & (ToolTag.LOCAL | ToolTag.PUBLIC)
assert definition.tags & ToolTag.LOCAL or definition.tags & ToolTag.PUBLIC
def test_public_surface_is_subset() -> None:
local = {t.name for t in default_tool_definitions(surface="local")}
public = {t.name for t in default_tool_definitions(surface="public")}
assert public
assert public <= local
# Todo series is the main PUBLIC surface today.
assert "todo_list" in public
assert "read_file" not in public
def test_read_only_classification() -> None:
"""READ_ONLY marks exactly the non-mutating builtins."""
register_builtin_tools()
read_only = {t.name for t in default_tool_definitions(surface="local") if t.tags & ToolTag.READ_ONLY}
assert read_only == {
"read_file",
"view_image",
"get_truncated",
"glob_paths",
"regex_files",
"listdir",
"tree",
"todo_list",
"wait",
"ask_user_line",
"ask_user_choice",
"ask_user_form",
"vcs_kind",
"vcs_status",
"vcs_diff",
"vcs_log",
"vcs_branch",
"fetch_local",
"skill_list",
"skill_read",
"skill_search",
}
def test_registry_clone_read_only_only() -> None:
"""clone(read_only_only=True) keeps only READ_ONLY tools, nothing else."""
register_builtin_tools()
full = ToolRegistry(default_tool_definitions(surface="local"))
ro = full.clone(read_only_only=True)
assert 0 < len(ro) < len(full)
assert {t.name for t in ro.definitions()} == {t.name for t in full.definitions() if t.tags & ToolTag.READ_ONLY}
assert all(t.tags & ToolTag.READ_ONLY for t in ro.definitions())
assert "run_argv" not in ro
assert "edit_replace" not in ro
# Plain clone keeps every tool.
assert len(full.clone()) == len(full)
def test_catalog_scope_empty_isolates_registration() -> None:
with catalog_scope(empty=True) as catalog:
@tool(name="only_in_scope", register=True)
async def only_in_scope() -> str:
return "ok"
_ = only_in_scope
assert catalog.get("only_in_scope") is not None
# default_tool_definitions re-seeds builtins into the override; plugin-like
# names from this scope stay local to the override and leave process catalog.
assert get_catalog().get("only_in_scope") is None
def test_collision_refuses_shadow() -> None:
catalog = ToolCatalog()
@tool(register=False)
async def alpha() -> str:
return "a"
catalog.register(alpha, source=ToolSource(kind="builtin"))
try:
catalog.register(alpha, source=ToolSource(kind="plugin", plugin_id="acme"))
raise AssertionError("expected collision")
except ValueError as exc:
assert "collision" in str(exc)
def test_registration_source_context() -> None:
with catalog_scope(empty=True) as catalog:
with registration_source(ToolSource(kind="plugin", plugin_id="acme")):
@tool(name="plugin_tool")
async def plugin_tool() -> str:
return "p"
_ = plugin_tool
entry = catalog.get("plugin_tool")
assert entry is not None
assert entry.source.kind == "plugin"
assert entry.source.plugin_id == "acme"
def test_tool_tags_reject_neither_surface() -> None:
try:
@tool(tags=ToolTag.YOLO, register=False)
async def bad() -> str:
return "x"
_ = bad
raise AssertionError("expected ValueError")
except ValueError as exc:
assert "LOCAL" in str(exc)