Files
wehub-resource-sync e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

410 lines
15 KiB
Python

"""Tests for built-in tools and unified tool registry behavior."""
from __future__ import annotations
import sys
import types
from types import SimpleNamespace
from typing import Any
import pytest
from deeptutor.core.tool_protocol import BaseTool, ToolDefinition, ToolParameter, ToolResult
from deeptutor.runtime.registry.tool_registry import ToolRegistry
from deeptutor.services.path_service import PathService
from deeptutor.services.sandbox.spec import ExecResult
from deeptutor.tools.builtin import (
BrainstormTool,
CodeExecutionTool,
ExecTool,
GeoGebraAnalysisTool,
PaperSearchToolWrapper,
RAGTool,
ReasonTool,
WebSearchTool,
)
def _install_module(
monkeypatch: pytest.MonkeyPatch, fullname: str, **attrs: Any
) -> types.ModuleType:
"""Install a fake module (and missing parent packages) into sys.modules."""
parts = fullname.split(".")
for idx in range(1, len(parts)):
pkg_name = ".".join(parts[:idx])
if pkg_name not in sys.modules:
pkg = types.ModuleType(pkg_name)
pkg.__path__ = [] # type: ignore[attr-defined]
monkeypatch.setitem(sys.modules, pkg_name, pkg)
if idx > 1:
parent = sys.modules[".".join(parts[: idx - 1])]
# Use monkeypatch (not raw setattr) so the parent package's
# attribute is restored on teardown — otherwise a real
# submodule the parent exposes (e.g. ``llm.config``) stays
# shadowed by the fake and leaks into later tests.
monkeypatch.setattr(parent, parts[idx - 1], pkg, raising=False)
module = types.ModuleType(fullname)
for key, value in attrs.items():
setattr(module, key, value)
monkeypatch.setitem(sys.modules, fullname, module)
if len(parts) > 1:
parent = sys.modules[".".join(parts[:-1])]
monkeypatch.setattr(parent, parts[-1], module, raising=False)
return module
@pytest.mark.asyncio
async def test_exec_tool_reports_generated_public_artifacts(
tmp_path, monkeypatch: pytest.MonkeyPatch
) -> None:
path_service = PathService(workspace_root=tmp_path / "data")
workdir = path_service.get_task_workspace("chat", "turn_1") / "exec"
class FakeSandboxService:
async def run(self, request, *, user_id: str):
assert user_id == "user-1"
assert request.command == "python build_pdf.py"
output_dir = PathService(workspace_root=tmp_path / "data").get_task_workspace(
"chat", "turn_1"
)
assert request.workdir == str(output_dir / "exec")
work = output_dir / "exec"
work.mkdir(parents=True, exist_ok=True)
(work / "report.pdf").write_bytes(b"%PDF-1.4\n")
(work / "build_pdf.py").write_text("print('internal')", encoding="utf-8")
return ExecResult(stdout="created report.pdf\n", exit_code=0)
import deeptutor.services.sandbox as sandbox_pkg
import deeptutor.services.sandbox.artifacts as sandbox_artifacts
monkeypatch.setattr(sandbox_pkg, "get_sandbox_service", lambda: FakeSandboxService())
monkeypatch.setattr(sandbox_artifacts, "get_path_service", lambda: path_service)
result = await ExecTool().execute(
command="python build_pdf.py",
_sandbox_user_id="user-1",
_sandbox_workdir=str(workdir),
)
assert result.success is True
assert "Generated artifacts" in result.content
assert "report.pdf" in result.content
# The model is told to mention the exact filename (the UI linkifies it); the
# raw download URL is delivered out-of-band (sources/metadata), never in the
# model-facing text, so the model can't paste it.
assert "clickable link" in result.content
assert "/api/outputs/" not in result.content
assert "build_pdf.py" not in result.content
assert result.metadata["artifacts"][0]["filename"] == "report.pdf"
assert (
result.metadata["artifacts"][0]["url"]
== "/api/outputs/workspace/chat/chat/turn_1/exec/report.pdf"
)
assert result.sources[0]["url"].endswith("/report.pdf")
@pytest.mark.asyncio
async def test_brainstorm_tool_passes_llm_arguments(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
async def fake_brainstorm(**kwargs: Any) -> dict[str, Any]:
captured.update(kwargs)
return {"answer": "## 1. Test idea\n- Rationale: worth exploring"}
_install_module(monkeypatch, "deeptutor.tools.brainstorm", brainstorm=fake_brainstorm)
result = await BrainstormTool().execute(
topic="agent-native tutoring",
context="Focus on fast ideation",
model="gpt-test",
)
assert "Test idea" in result.content
assert captured["topic"] == "agent-native tutoring"
assert captured["context"] == "Focus on fast ideation"
assert captured["model"] == "gpt-test"
@pytest.mark.asyncio
async def test_rag_tool_forwards_query_and_extra_kwargs(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
async def fake_rag_search(**kwargs: Any) -> dict[str, Any]:
captured.update(kwargs)
return {"answer": "grounded answer", "provider": "fake"}
_install_module(monkeypatch, "deeptutor.tools.rag_tool", rag_search=fake_rag_search)
result = await RAGTool().execute(
query="what is a tensor",
kb_name="demo-kb",
mode="hybrid",
only_need_context=True,
)
assert result.content == "grounded answer"
assert captured["query"] == "what is a tensor"
assert captured["kb_name"] == "demo-kb"
assert captured["mode"] == "hybrid"
assert captured["only_need_context"] is True
@pytest.mark.asyncio
async def test_rag_tool_rejects_empty_query(monkeypatch: pytest.MonkeyPatch) -> None:
called = False
async def fake_rag_search(**_kwargs: Any) -> dict[str, Any]:
nonlocal called
called = True
return {"answer": "should not run"}
_install_module(monkeypatch, "deeptutor.tools.rag_tool", rag_search=fake_rag_search)
with pytest.raises(ValueError, match="RAG query must be a non-empty string"):
await RAGTool().execute(query=" ", kb_name="demo-kb")
assert called is False
@pytest.mark.asyncio
async def test_web_search_tool_wraps_sync_function(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
def fake_web_search(**kwargs: Any) -> dict[str, Any]:
captured.update(kwargs)
return {
"answer": "web summary",
"citations": [{"url": "https://example.com", "title": "Example"}],
}
_install_module(monkeypatch, "deeptutor.tools.web_search", web_search=fake_web_search)
result = await WebSearchTool().execute(query="latest benchmark", output_dir="/tmp/out")
assert result.content == "web summary"
assert captured["query"] == "latest benchmark"
assert captured["output_dir"] == "/tmp/out"
assert result.sources[0]["url"] == "https://example.com"
@pytest.mark.asyncio
async def test_code_execution_tool_runs_python_via_sandbox(
tmp_path, monkeypatch: pytest.MonkeyPatch
) -> None:
from pathlib import Path
path_service = PathService(workspace_root=tmp_path / "data")
workdir = path_service.get_task_workspace("chat", "turn_1") / "code_runs"
class FakeSandboxService:
async def run(self, request, *, user_id: str):
assert user_id == "user-1"
assert request.command == "python3 main.py"
run_dir = Path(request.workdir)
assert run_dir.parent == workdir
assert run_dir.name.startswith("python_")
# The tool wrote the source file into the run dir before invoking us.
assert (run_dir / "main.py").read_text(encoding="utf-8") == "print(2 + 2)"
(run_dir / "result.txt").write_text("ok", encoding="utf-8")
return ExecResult(stdout="4\n", exit_code=0)
import deeptutor.services.sandbox as sandbox_pkg
import deeptutor.services.sandbox.artifacts as sandbox_artifacts
monkeypatch.setattr(sandbox_pkg, "get_sandbox_service", lambda: FakeSandboxService())
monkeypatch.setattr(sandbox_artifacts, "get_path_service", lambda: path_service)
result = await CodeExecutionTool().execute(
language="python",
code="print(2 + 2)",
_sandbox_user_id="user-1",
_sandbox_workdir=str(workdir),
)
assert result.success is True
assert "4" in result.content
assert result.metadata["language"] == "python"
assert result.metadata["command"] == "python3 main.py"
# The program-generated file surfaces; the source file we wrote is filtered.
artifact_names = [row["filename"] for row in result.metadata["artifacts"]]
assert "result.txt" in artifact_names
assert "main.py" not in artifact_names
@pytest.mark.asyncio
async def test_code_execution_tool_compiles_cpp(tmp_path, monkeypatch: pytest.MonkeyPatch) -> None:
from pathlib import Path
path_service = PathService(workspace_root=tmp_path / "data")
workdir = path_service.get_task_workspace("chat", "turn_1") / "code_runs"
captured: dict[str, Any] = {}
class FakeSandboxService:
async def run(self, request, *, user_id: str):
captured["command"] = request.command
run_dir = Path(request.workdir)
assert run_dir.name.startswith("cpp_")
assert (run_dir / "main.cpp").exists()
return ExecResult(stdout="hi\n", exit_code=0)
import deeptutor.services.sandbox as sandbox_pkg
import deeptutor.services.sandbox.artifacts as sandbox_artifacts
monkeypatch.setattr(sandbox_pkg, "get_sandbox_service", lambda: FakeSandboxService())
monkeypatch.setattr(sandbox_artifacts, "get_path_service", lambda: path_service)
result = await CodeExecutionTool().execute(
language="cpp",
code="int main(){}",
_sandbox_workdir=str(workdir),
)
assert result.success is True
assert captured["command"] == "c++ -std=c++17 -O2 main.cpp -o prog && ./prog"
@pytest.mark.asyncio
async def test_code_execution_tool_rejects_bad_input() -> None:
tool = CodeExecutionTool()
with pytest.raises(ValueError, match="Unsupported language"):
await tool.execute(language="ruby", code="puts 1")
with pytest.raises(ValueError, match="non-empty 'code'"):
await tool.execute(language="python", code=" ")
@pytest.mark.asyncio
async def test_reason_tool_passes_llm_arguments(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
async def fake_reason(**kwargs: Any) -> dict[str, Any]:
captured.update(kwargs)
return {"answer": "reasoned"}
_install_module(monkeypatch, "deeptutor.tools.reason", reason=fake_reason)
result = await ReasonTool().execute(
query="derive the formula",
context="prior work",
api_key="key",
base_url="url",
model="gpt-test",
)
assert result.content == "reasoned"
assert captured["model"] == "gpt-test"
assert captured["context"] == "prior work"
@pytest.mark.asyncio
async def test_paper_search_tool_formats_papers(monkeypatch: pytest.MonkeyPatch) -> None:
class FakeArxivSearchTool:
async def search_papers(self, **kwargs: Any) -> list[dict[str, Any]]:
assert kwargs["query"] == "graph learning"
return [
{
"title": "Graph Learning 101",
"year": 2024,
"authors": ["Ada", "Grace"],
"arxiv_id": "1234.5678",
"url": "https://arxiv.org/abs/1234.5678",
"abstract": "A compact abstract.",
}
]
_install_module(
monkeypatch,
"deeptutor.tools.paper_search_tool",
ArxivSearchTool=FakeArxivSearchTool,
)
result = await PaperSearchToolWrapper().execute(query="graph learning")
assert "Graph Learning 101" in result.content
assert result.sources[0]["provider"] == "arxiv"
@pytest.mark.asyncio
async def test_geogebra_analysis_tool_handles_success(monkeypatch: pytest.MonkeyPatch) -> None:
class FakeVisionSolverAgent:
def __init__(self, **kwargs: Any) -> None:
self.kwargs = kwargs
async def process(self, **kwargs: Any) -> dict[str, Any]:
assert kwargs["question_text"] == "analyze this"
return {
"has_image": True,
"final_ggb_commands": ["A=(0,0)", "B=(1,0)"],
"analysis_output": {
"constraints": ["AB = 1"],
"geometric_relations": [{"description": "A and B are on x-axis"}],
},
"image_is_reference": False,
}
def format_ggb_block(self, commands: list[str]) -> str:
return "\n".join(commands)
_install_module(
monkeypatch,
"deeptutor.agents.vision_solver.vision_solver_agent",
VisionSolverAgent=FakeVisionSolverAgent,
)
_install_module(
monkeypatch,
"deeptutor.services.llm.config",
get_llm_config=lambda: SimpleNamespace(api_key="k", base_url="u"),
)
result = await GeoGebraAnalysisTool().execute(
question="analyze this",
image_base64="ZmFrZQ==",
language="en",
)
assert result.success is True
assert "A=(0,0)" in result.content
assert result.metadata["commands_count"] == 2
@pytest.mark.asyncio
async def test_tool_registry_resolves_aliases_and_argument_mapping() -> None:
class DummyTool(BaseTool):
def __init__(self, tool_name: str) -> None:
self._tool_name = tool_name
self.calls: list[dict[str, Any]] = []
def get_definition(self) -> ToolDefinition:
param_name = {
"rag": "query",
"code_execution": "code",
}[self._tool_name]
return ToolDefinition(
name=self._tool_name,
description="dummy",
parameters=[ToolParameter(name=param_name, type="string")],
)
async def execute(self, **kwargs: Any) -> ToolResult:
self.calls.append(kwargs)
return ToolResult(content=self._tool_name)
rag = DummyTool("rag")
code = DummyTool("code_execution")
registry = ToolRegistry()
registry.register(rag)
registry.register(code)
rag_result = await registry.execute("rag_hybrid", query="find this")
code_result = await registry.execute("run_code", language="python", code="print(1)")
assert rag_result.content == "rag"
assert rag.calls[0]["mode"] == "hybrid"
assert rag.calls[0]["query"] == "find this"
# The `run_code` alias resolves to code_execution and forwards kwargs
# verbatim — no natural-language `query`→`intent` remapping any more.
assert code_result.content == "code_execution"
assert code.calls[0]["code"] == "print(1)"
assert code.calls[0]["language"] == "python"