97 findings to zero. Most were mechanical — 52 unsorted import blocks, 10 unsorted __all__, assorted pyupgrade and simplify hints. Two were not, and both were visible only because the lint made me look. `webber version` did not exist. src/cli/commands/version.py defines show_version(), main.py imported it, and the registration line was never written — the CLI exposed chat and explore only. The import carried `# noqa: F401`, which is what kept the omission quiet: someone marked the symptom as intentional instead of asking why it was unused. show_version is not redundant with the --version flag; it prints the resolved Ollama URL, model and debug state, which is the form worth having when something is misconfigured. Registered, and the suppression dropped because the import is now genuinely used. test_spawn_explore_agent asserted nothing. It built a mock RunContext, patched get_agent, and stopped at the comment "For now, verify the explore agent would be called correctly". It had been counted as a passing test. An AST sweep of all 238 test functions found it was the only one, which is worth knowing — the problem was contained, not systemic. It is now skipped with a reason, so it reports as unfinished rather than as passing. Reducing it rather than deleting its imports was the point: tidying the imports would have made a hollow test look clean. Two findings were false positives, and both are recorded rather than silently worked around: B023 flagged run_agent closing over full_prompt and ctx. Traced: agent_task is awaited at line 326 before `continue` reaches the next iteration, so neither name can be rebound while the closure is pending, and the exception path cancels and awaits too. Not a bug. Bound as defaults anyway, because that stays true if the await ever moves. I had called it a live bug before tracing it, which is the mistake Rule 5 exists for. RUF012 flagged `rules: list[ApprovalRule] = []` on ApprovalRuleSet. Its suggested fix — annotate ClassVar — would remove the field from the model. ApprovalRuleSet is a pydantic model and pydantic deep-copies defaults per instance; verified by constructing two and confirming their lists are distinct objects. Suppressed with that evidence in the comment. Ruff cannot see the pydantic base because BaseSchema is a local subclass of BaseModel. Also moved a stray `from src.shared.logging import ...` that had drifted below a function definition, and merged a nested if in the ollama provider. 215 passed, 23 skipped, unchanged except for the new skip. `webber version` exercised end to end. mypy is NOT addressed here and the gate still fails on it — 55 errors in 14 files, 35 of them no-any-return from pydantic_ai's untyped returns. That was hidden behind ruff, because the gate stops at the first failing stage. Co-Authored-By: Claude <noreply@anthropic.com>
153 lines
4.9 KiB
Python
153 lines
4.9 KiB
Python
"""
|
|
Tests for the Plan agent.
|
|
|
|
Tests registration, API endpoints, and tool restrictions.
|
|
"""
|
|
import pytest
|
|
|
|
from src.domains.agents.base import get_agent, list_agents
|
|
from src.domains.agents.plan import PlanAgentImpl, plan_agent
|
|
|
|
|
|
class TestPlanAgentRegistration:
|
|
"""Tests for Plan agent registration."""
|
|
|
|
def test_plan_agent_registered(self):
|
|
"""Test that plan agent is registered in registry."""
|
|
agent = get_agent("plan")
|
|
assert agent is not None
|
|
assert agent.name == "plan"
|
|
|
|
def test_plan_agent_in_list(self):
|
|
"""Test that plan agent appears in agent list."""
|
|
agents = list_agents()
|
|
names = [a["name"] for a in agents]
|
|
assert "plan" in names
|
|
|
|
def test_plan_agent_has_description(self):
|
|
"""Test that plan agent has a description."""
|
|
agent = get_agent("plan")
|
|
assert agent is not None
|
|
assert len(agent.description) > 0
|
|
assert "plan" in agent.description.lower() or "architect" in agent.description.lower()
|
|
|
|
def test_plan_agent_singleton(self):
|
|
"""Test that plan_agent is the registered instance."""
|
|
registered = get_agent("plan")
|
|
assert registered is plan_agent
|
|
|
|
def test_plan_agent_is_correct_type(self):
|
|
"""Test that plan agent is correct implementation type."""
|
|
assert isinstance(plan_agent, PlanAgentImpl)
|
|
|
|
|
|
class TestPlanAgentTools:
|
|
"""Tests for Plan agent tool restrictions."""
|
|
|
|
def test_plan_agent_has_read_only_tools(self):
|
|
"""Test that plan agent has read-only tools."""
|
|
# Access the underlying PydanticAI agent to check tools
|
|
agent = plan_agent.agent
|
|
tool_names = list(agent._function_toolset.tools.keys())
|
|
|
|
# Should have read-only tools
|
|
assert "read_file" in tool_names
|
|
assert "glob_files" in tool_names
|
|
assert "grep_content" in tool_names
|
|
assert "bash_readonly" in tool_names
|
|
|
|
def test_plan_agent_no_write_tools(self):
|
|
"""Test that plan agent does NOT have write tools."""
|
|
agent = plan_agent.agent
|
|
tool_names = list(agent._function_toolset.tools.keys())
|
|
|
|
# Should NOT have write tools
|
|
assert "edit_file" not in tool_names
|
|
assert "write_file" not in tool_names
|
|
assert "bash" not in tool_names
|
|
assert "web_search" not in tool_names
|
|
|
|
def test_plan_agent_tool_count(self):
|
|
"""Test that plan agent has exactly 4 tools."""
|
|
agent = plan_agent.agent
|
|
tool_count = len(agent._function_toolset.tools)
|
|
assert tool_count == 4
|
|
|
|
|
|
class TestPlanAgentAPI:
|
|
"""Tests for Plan agent REST API."""
|
|
|
|
@pytest.mark.anyio
|
|
async def test_list_agents_includes_plan(self, auth_client):
|
|
"""Test that agent list includes plan agent."""
|
|
response = await auth_client.get("/agents/")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
names = [a["name"] for a in data["agents"]]
|
|
assert "plan" in names
|
|
|
|
@pytest.mark.anyio
|
|
async def test_get_plan_agent_info(self, auth_client):
|
|
"""Test getting plan agent info."""
|
|
response = await auth_client.get("/agents/plan")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["name"] == "plan"
|
|
assert "description" in data
|
|
assert len(data["description"]) > 0
|
|
|
|
@pytest.mark.anyio
|
|
async def test_run_plan_with_invalid_body(self, auth_client):
|
|
"""Test running plan agent with invalid request."""
|
|
response = await auth_client.post(
|
|
"/agents/run",
|
|
json={
|
|
"agent_type": "plan",
|
|
# Missing prompt
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 422
|
|
|
|
@pytest.mark.anyio
|
|
async def test_stream_plan_with_invalid_body(self, auth_client):
|
|
"""Test streaming plan agent with invalid request."""
|
|
response = await auth_client.post(
|
|
"/agents/stream",
|
|
json={
|
|
"agent_type": "plan",
|
|
# Missing prompt
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 422
|
|
|
|
|
|
class TestPlanAgentProperties:
|
|
"""Tests for Plan agent properties and configuration."""
|
|
|
|
def test_plan_agent_name(self):
|
|
"""Test plan agent name property."""
|
|
assert plan_agent.name == "plan"
|
|
|
|
def test_plan_agent_description_not_empty(self):
|
|
"""Test plan agent description is not empty."""
|
|
assert plan_agent.description
|
|
assert len(plan_agent.description) > 10
|
|
|
|
def test_plan_agent_creates_agent_lazily(self):
|
|
"""Test that PydanticAI agent is created lazily."""
|
|
# Create a fresh instance
|
|
fresh_agent = PlanAgentImpl()
|
|
|
|
# _agent should be None before first access
|
|
assert fresh_agent._agent is None
|
|
|
|
# Access the agent property
|
|
_ = fresh_agent.agent
|
|
|
|
# Now _agent should be set
|
|
assert fresh_agent._agent is not None
|