fix(webber-api): clear ruff, and two things it was pointing at

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>
This commit is contained in:
2026-08-11 15:05:02 +02:00
co-authored by Claude
parent 9f5e331d11
commit eb3467d06a
50 changed files with 168 additions and 160 deletions
+1 -1
View File
@@ -6,9 +6,9 @@ from pathlib import Path
import typer
from src.cli.session.loop import AgenticLoop
from src.cli.theme import get_theme
from src.cli.ui.console import get_console
from src.cli.session.loop import AgenticLoop
from src.shared.logging import setup_logging
console = get_console()
+7 -1
View File
@@ -53,11 +53,17 @@ def main(
# Import and register commands
from src.cli.commands import chat, explore, version # noqa: E402, F401
from src.cli.commands import chat, explore, version # noqa: E402
# Register subcommands
app.command(name="chat")(chat.chat_command)
app.command(name="explore")(explore.explore_command)
# version was imported and never registered, so `webber version` did not exist.
# The --version flag above is the terse form; show_version prints the panel with
# the resolved Ollama URL, model and debug state, which is the one worth having
# when something is misconfigured. The F401 suppression on the import was what
# kept the omission quiet.
app.command(name="version")(version.show_version)
if __name__ == "__main__":
+1 -1
View File
@@ -4,4 +4,4 @@ Session management for CLI.
from src.cli.session.context import SessionState
from src.cli.session.loop import AgenticLoop
__all__ = ["SessionState", "AgenticLoop"]
__all__ = ["AgenticLoop", "SessionState"]
+1 -2
View File
@@ -1,14 +1,13 @@
"""
Agentic conversation loop for interactive CLI.
"""
from typing import Any
from rich.console import Console
from src.cli.session.context import SessionState
from src.cli.ui.display import format_response
from src.domains.agents.base import BaseAgent
from src.shared.logging import logged, trace_span, get_logger
from src.shared.logging import get_logger, logged, trace_span
logger = get_logger(__name__)
+2 -2
View File
@@ -4,7 +4,7 @@ CLI theme configuration.
Centralized color and style definitions for the Webber CLI.
All color choices should be defined here for easy customization.
"""
from dataclasses import dataclass
from dataclasses import dataclass, field
@dataclass(frozen=True)
@@ -36,7 +36,7 @@ class ThemeColors:
class ThemeConfig:
"""Complete theme configuration."""
colors: ThemeColors = ThemeColors()
colors: ThemeColors = field(default_factory=ThemeColors)
# Spinner style for loading indicators
spinner: str = "dots"
+2 -2
View File
@@ -2,6 +2,6 @@
CLI UI components.
"""
from src.cli.ui.console import get_console
from src.cli.ui.display import format_response, format_code
from src.cli.ui.display import format_code, format_response
__all__ = ["get_console", "format_response", "format_code"]
__all__ = ["format_code", "format_response", "get_console"]
-1
View File
@@ -8,7 +8,6 @@ from rich.syntax import Syntax
from rich.text import Text
from src.cli.theme import get_theme
from src.cli.ui.console import get_console
def format_response(text: str) -> Markdown | Text:
+1 -1
View File
@@ -7,8 +7,8 @@ from src.db.database import Database, get_database, get_session
from src.db.models import Base
__all__ = [
"Base",
"Database",
"get_database",
"get_session",
"Base",
]
+12 -12
View File
@@ -4,34 +4,34 @@ Agent implementations.
All agents inherit from BaseAgent and are registered in the global registry.
"""
from src.domains.agents.base import (
BaseAgent,
AgentContext,
AgentProtocol,
register_agent,
BaseAgent,
get_agent,
list_agents,
get_registry,
list_agents,
register_agent,
)
from src.domains.agents.explore import (
ExploreAgentImpl,
ExploreContext,
explore_agent,
explore,
explore_agent,
)
__all__ = [
# Base classes
"BaseAgent",
"AgentContext",
"AgentProtocol",
# Registry functions
"register_agent",
"get_agent",
"list_agents",
"get_registry",
# Base classes
"BaseAgent",
# Explore agent
"ExploreAgentImpl",
"ExploreContext",
"explore_agent",
"explore",
"explore_agent",
"get_agent",
"get_registry",
"list_agents",
# Registry functions
"register_agent",
]
@@ -4,13 +4,13 @@ Explore Agent - Fast codebase exploration.
from src.domains.agents.explore.agent import (
ExploreAgentImpl,
ExploreContext,
explore_agent,
explore,
explore_agent,
)
__all__ = [
"ExploreAgentImpl",
"ExploreContext",
"explore_agent",
"explore",
"explore_agent",
]
@@ -12,11 +12,11 @@ from typing import Any
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIModel
from src.domains.agents.base import BaseAgent, AgentContext, register_agent
from src.domains.agents.base import AgentContext, BaseAgent, register_agent
from src.domains.agents.explore.prompts import EXPLORE_SYSTEM_PROMPT
from src.ollama.provider import get_ollama_provider
from src.shared.config import get_settings
from src.shared.logging import logged, get_logger, trace_span
from src.shared.logging import get_logger, logged, trace_span
logger = get_logger(__name__)
@@ -6,9 +6,9 @@ Registers our tool implementations with the PydanticAI agent.
from pydantic_ai import Agent, RunContext
from src.domains.agents.base import AgentContext
from src.domains.tools.file.read import ReadFileTool
from src.domains.tools.file.glob import GlobFilesTool
from src.domains.tools.file.edit import EditFileTool
from src.domains.tools.file.glob import GlobFilesTool
from src.domains.tools.file.read import ReadFileTool
from src.domains.tools.file.write import WriteFileTool
from src.domains.tools.search.grep import GrepContentTool
from src.domains.tools.search.web import WebSearchTool
@@ -16,15 +16,15 @@ Usage:
from src.domains.agents.plan.agent import (
PlanAgentImpl,
PlanContext,
plan_agent,
plan,
plan_agent,
plan_stream,
)
__all__ = [
"PlanAgentImpl",
"PlanContext",
"plan_agent",
"plan",
"plan_agent",
"plan_stream",
]
+2 -2
View File
@@ -12,11 +12,11 @@ from typing import Any
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIModel
from src.domains.agents.base import BaseAgent, AgentContext, register_agent
from src.domains.agents.base import AgentContext, BaseAgent, register_agent
from src.domains.agents.plan.prompts import PLAN_SYSTEM_PROMPT
from src.ollama.provider import get_ollama_provider
from src.shared.config import get_settings
from src.shared.logging import logged, get_logger, trace_span
from src.shared.logging import get_logger, logged, trace_span
logger = get_logger(__name__)
+1 -1
View File
@@ -7,8 +7,8 @@ It cannot modify files - only explore and analyze.
from pydantic_ai import Agent, RunContext
from src.domains.agents.base import AgentContext
from src.domains.tools.file.read import ReadFileTool
from src.domains.tools.file.glob import GlobFilesTool
from src.domains.tools.file.read import ReadFileTool
from src.domains.tools.search.grep import GrepContentTool
from src.domains.tools.shell.bash import BashReadOnlyTool
+7 -7
View File
@@ -10,23 +10,24 @@ Streaming uses structured events instead of raw text to avoid
garbled output during tool execution.
"""
import json
from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse
from src.domains.agents.base import get_agent, list_agents
# Import agents to ensure they're registered
import src.domains.agents.explore # noqa: F401
import src.domains.agents.plan # noqa: F401
import src.domains.agents.explore
import src.domains.agents.plan
import src.domains.agents.task # noqa: F401
from src.domains.agents.base import get_agent, list_agents
from src.domains.agents.schemas import (
AgentRunRequest,
AgentRunResponse,
AgentInfo,
AgentListResponse,
AgentRunRequest,
AgentRunResponse,
PermissionMode,
StreamEvent,
)
from src.shared.logging import get_logger, logged
def _get_mode(mode_value: str | PermissionMode) -> PermissionMode:
@@ -34,7 +35,6 @@ def _get_mode(mode_value: str | PermissionMode) -> PermissionMode:
if isinstance(mode_value, PermissionMode):
return mode_value
return PermissionMode(mode_value)
from src.shared.logging import logged, get_logger
logger = get_logger(__name__)
+7 -1
View File
@@ -70,7 +70,13 @@ class ApprovalRuleSet(BaseSchema):
First matching rule determines the action.
If no rules match, falls back to default action.
"""
rules: list[ApprovalRule] = []
# Suppression justified: this is a pydantic model, not a plain class. Pydantic
# deep-copies field defaults per instance — verified: two ApprovalRuleSet()
# instances have `rules` lists that are not the same object, and appending
# to one leaves the other empty. RUF012's suggested fix, annotating this
# ClassVar, would remove the field from the model altogether. Ruff cannot
# see the pydantic base because BaseSchema is a local subclass of BaseModel.
rules: list[ApprovalRule] = [] # noqa: RUF012
default_action: ApprovalAction = ApprovalAction.ask # Default when no rules match
@@ -19,15 +19,15 @@ Usage:
from src.domains.agents.task.agent import (
TaskAgentImpl,
TaskContext,
task_agent,
task,
task_agent,
task_stream,
)
__all__ = [
"TaskAgentImpl",
"TaskContext",
"task_agent",
"task",
"task_agent",
"task_stream",
]
+17 -10
View File
@@ -8,6 +8,7 @@ Full orchestrator agent that can:
- Stream structured events instead of raw text
"""
import asyncio
import contextlib
import os
from collections.abc import AsyncIterator
from dataclasses import dataclass, field
@@ -16,12 +17,12 @@ from typing import Any
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIModel
from src.domains.agents.base import BaseAgent, AgentContext, register_agent
from src.domains.agents.base import AgentContext, BaseAgent, register_agent
from src.domains.agents.schemas import PermissionMode, StreamEvent, StreamEventType
from src.domains.agents.task.prompts import TASK_SYSTEM_PROMPT, TASK_PLAN_MODE_PROMPT
from src.domains.agents.task.prompts import TASK_PLAN_MODE_PROMPT, TASK_SYSTEM_PROMPT
from src.ollama.provider import get_ollama_provider
from src.shared.config import get_settings
from src.shared.logging import logged, get_logger, trace_span
from src.shared.logging import get_logger, logged, trace_span
logger = get_logger(__name__)
@@ -127,8 +128,8 @@ class TaskAgentImpl(BaseAgent):
def _register_tools(self, agent: Agent[TaskContext, str], mode: PermissionMode) -> None:
"""Register tools with the agent based on permission mode."""
from src.domains.agents.task.tools_streaming import (
register_task_tools_streaming,
register_readonly_tools_streaming,
register_task_tools_streaming,
)
if mode == PermissionMode.plan:
@@ -293,8 +294,16 @@ User request: {prompt}"""
message=f"Retrying (attempt {retries + 1})..."
)
# Run agent in background task so we can yield events
async def run_agent() -> str:
# Run agent in background task so we can yield events.
#
# full_prompt and ctx are bound as defaults rather than closed
# over. Today the closure is safe either way — the task is
# awaited below before `continue` reaches the next iteration, so
# neither name can be rebound while it is pending. Binding them
# keeps that true if the await ever moves, which is the failure
# B023 is warning about and the kind that surfaces as one agent
# silently running another's prompt.
async def run_agent(full_prompt: str = full_prompt, ctx: TaskContext = ctx) -> str:
try:
result = await agent.run(full_prompt, deps=ctx)
return result.output
@@ -314,7 +323,7 @@ User request: {prompt}"""
timeout=0.1
)
yield event
except asyncio.TimeoutError:
except TimeoutError:
# No events, check if agent is done
continue
@@ -346,10 +355,8 @@ User request: {prompt}"""
# Cancel agent if still running
if not agent_task.done():
agent_task.cancel()
try:
with contextlib.suppress(asyncio.CancelledError):
await agent_task
except asyncio.CancelledError:
pass
return
# Yield response in chunks for streaming feel
+2 -2
View File
@@ -8,9 +8,9 @@ The Task agent has access to tools based on permission mode:
from pydantic_ai import Agent, RunContext
from src.domains.agents.base import AgentContext
from src.domains.tools.file.read import ReadFileTool
from src.domains.tools.file.glob import GlobFilesTool
from src.domains.tools.file.edit import EditFileTool
from src.domains.tools.file.glob import GlobFilesTool
from src.domains.tools.file.read import ReadFileTool
from src.domains.tools.file.write import WriteFileTool
from src.domains.tools.search.grep import GrepContentTool
from src.domains.tools.search.web import WebSearchTool
@@ -9,9 +9,9 @@ from pydantic_ai import Agent, RunContext
from src.domains.agents.base import AgentContext
from src.domains.agents.schemas import StreamEvent, StreamEventType
from src.domains.agents.task.agent import TaskContext
from src.domains.tools.file.read import ReadFileTool
from src.domains.tools.file.glob import GlobFilesTool
from src.domains.tools.file.edit import EditFileTool
from src.domains.tools.file.glob import GlobFilesTool
from src.domains.tools.file.read import ReadFileTool
from src.domains.tools.file.write import WriteFileTool
from src.domains.tools.search.grep import GrepContentTool
from src.domains.tools.search.web import WebSearchTool
@@ -11,6 +11,6 @@ from src.domains.conversations.service import ConversationService
__all__ = [
"Conversation",
"Message",
"ConversationService",
"Message",
]
@@ -20,7 +20,7 @@ from src.domains.conversations.schemas import (
)
from src.domains.conversations.service import ConversationService
from src.shared.auth import require_auth
from src.shared.logging import logged, get_logger
from src.shared.logging import get_logger, logged
logger = get_logger(__name__)
@@ -170,7 +170,7 @@ async def add_message(
logger.exception(f"Agent response failed: {e}")
raise HTTPException(
status_code=500,
detail=f"Agent error: {str(e)}"
detail=f"Agent error: {e!s}"
)
# Add assistant message
@@ -6,7 +6,6 @@ from uuid import UUID
from pydantic import BaseModel, Field
# === Request Schemas ===
class CreateConversationRequest(BaseModel):
@@ -5,7 +5,7 @@ Handles CRUD operations, context building, and summarization triggers.
"""
from uuid import UUID
from sqlalchemy import select, func
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
+1 -1
View File
@@ -6,9 +6,9 @@ main.py only includes this root_router.
"""
from fastapi import APIRouter
from src.domains.health.router import router as health_router
from src.domains.agents.router import router as agents_router
from src.domains.conversations.router import router as conversations_router
from src.domains.health.router import router as health_router
# from src.domains.auth.router import router as auth_router
# from src.domains.tools.router import router as tools_router
+8 -8
View File
@@ -4,19 +4,19 @@ Tool implementations for agent use.
All tools inherit from BaseTool and return ToolResult.
"""
from src.domains.tools.base import BaseTool, ToolResult
from src.domains.tools.file import ReadFileTool, GlobFilesTool, EditFileTool, WriteFileTool
from src.domains.tools.file import EditFileTool, GlobFilesTool, ReadFileTool, WriteFileTool
from src.domains.tools.search import GrepContentTool, WebSearchTool
from src.domains.tools.shell import BashReadOnlyTool, BashTool
__all__ = [
"BaseTool",
"ToolResult",
"ReadFileTool",
"GlobFilesTool",
"EditFileTool",
"WriteFileTool",
"GrepContentTool",
"WebSearchTool",
"BashReadOnlyTool",
"BashTool",
"EditFileTool",
"GlobFilesTool",
"GrepContentTool",
"ReadFileTool",
"ToolResult",
"WebSearchTool",
"WriteFileTool",
]
+1 -4
View File
@@ -32,10 +32,7 @@ class ToolResult:
if not self.success:
return f"ERROR: {self.error}"
if isinstance(self.data, str):
content = self.data
else:
content = str(self.data)
content = self.data if isinstance(self.data, str) else str(self.data)
if len(content) > max_length:
self.truncated = True
@@ -1,9 +1,9 @@
"""
File operation tools.
"""
from src.domains.tools.file.read import ReadFileTool
from src.domains.tools.file.glob import GlobFilesTool
from src.domains.tools.file.edit import EditFileTool
from src.domains.tools.file.glob import GlobFilesTool
from src.domains.tools.file.read import ReadFileTool
from src.domains.tools.file.write import WriteFileTool
__all__ = ["ReadFileTool", "GlobFilesTool", "EditFileTool", "WriteFileTool"]
__all__ = ["EditFileTool", "GlobFilesTool", "ReadFileTool", "WriteFileTool"]
+6 -5
View File
@@ -2,11 +2,12 @@
File editing tool with find-and-replace functionality.
"""
import difflib
import aiofiles
from pathlib import Path
import aiofiles
from src.domains.tools.base import BaseTool, ToolResult
from src.shared.logging import logged, get_logger
from src.shared.logging import get_logger, logged
logger = get_logger(__name__)
@@ -147,15 +148,15 @@ Examples:
try:
# Read file content
async with aiofiles.open(path, 'r', encoding='utf-8', errors='replace') as f:
async with aiofiles.open(path, encoding='utf-8', errors='replace') as f:
content = await f.read()
# Check if old_string exists
count = content.count(old_string)
if count == 0:
return self._error(
f"old_string not found in file. "
f"Make sure to match exact whitespace and indentation."
"old_string not found in file. "
"Make sure to match exact whitespace and indentation."
)
# Check uniqueness if replace_all is False
+1 -1
View File
@@ -6,7 +6,7 @@ from pathlib import Path
from src.domains.tools.base import BaseTool, ToolResult
from src.domains.tools.gitignore import filter_gitignored
from src.shared.logging import logged, get_logger
from src.shared.logging import get_logger, logged
logger = get_logger(__name__)
+4 -3
View File
@@ -1,11 +1,12 @@
"""
File reading tool with line number formatting and sandboxing.
"""
import aiofiles
from pathlib import Path
import aiofiles
from src.domains.tools.base import BaseTool, ToolResult
from src.shared.logging import logged, get_logger
from src.shared.logging import get_logger, logged
logger = get_logger(__name__)
@@ -87,7 +88,7 @@ IMPORTANT:
return self._error(f"Not a file: {file_path}")
try:
async with aiofiles.open(path, 'r', encoding='utf-8', errors='replace') as f:
async with aiofiles.open(path, encoding='utf-8', errors='replace') as f:
content = await f.read()
lines = content.splitlines()
+3 -2
View File
@@ -1,11 +1,12 @@
"""
File writing tool for creating and overwriting files.
"""
import aiofiles
from pathlib import Path
import aiofiles
from src.domains.tools.base import BaseTool, ToolResult
from src.shared.logging import logged, get_logger
from src.shared.logging import get_logger, logged
logger = get_logger(__name__)
+1 -4
View File
@@ -90,10 +90,7 @@ class GitignoreFilter:
# Make path relative to root for matching
try:
if path.is_absolute():
rel_path = path.resolve().relative_to(self.root_dir)
else:
rel_path = path
rel_path = path.resolve().relative_to(self.root_dir) if path.is_absolute() else path
except ValueError:
# Path is not under root_dir, don't filter
return False
+1 -1
View File
@@ -7,7 +7,7 @@ from typing import Literal
from src.domains.tools.base import BaseTool, ToolResult
from src.domains.tools.gitignore import filter_gitignored
from src.shared.logging import logged, get_logger
from src.shared.logging import get_logger, logged
logger = get_logger(__name__)
+1 -1
View File
@@ -8,7 +8,7 @@ import httpx
from src.domains.tools.base import BaseTool, ToolResult
from src.shared.config import get_settings
from src.shared.logging import logged, get_logger
from src.shared.logging import get_logger, logged
from src.shared.retry import retry_async
logger = get_logger(__name__)
+2 -2
View File
@@ -8,7 +8,7 @@ import shlex
from pathlib import Path
from src.domains.tools.base import BaseTool, ToolResult
from src.shared.logging import logged, get_logger
from src.shared.logging import get_logger, logged
logger = get_logger(__name__)
@@ -190,7 +190,7 @@ Examples:
exit_code=proc.returncode
)
except asyncio.TimeoutError:
except TimeoutError:
return self._error(f"Command timed out after {timeout} seconds")
except Exception as e:
logger.exception(f"Error executing command: {command}")
@@ -8,7 +8,7 @@ import shlex
from pathlib import Path
from src.domains.tools.base import BaseTool, ToolResult
from src.shared.logging import logged, get_logger
from src.shared.logging import get_logger, logged
logger = get_logger(__name__)
@@ -228,7 +228,7 @@ Examples:
exit_code=proc.returncode
)
except asyncio.TimeoutError:
except TimeoutError:
return self._error(f"Command timed out after {timeout} seconds")
except Exception as e:
logger.exception(f"Error executing command: {command}")
@@ -340,7 +340,7 @@ Examples:
# Handle git with flags before subcommand (e.g., git -C path status)
if git_subcommand.startswith("-"):
# Find the actual subcommand
for i, token in enumerate(tokens[2:], 2):
for _i, token in enumerate(tokens[2:], 2):
if not token.startswith("-"):
git_subcommand = token
break
+4 -2
View File
@@ -110,8 +110,10 @@ def _sanitize_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
msg_copy = dict(msg)
# Fix null content in assistant messages with tool calls
if msg_copy.get("role") == "assistant":
if msg_copy.get("content") is None and msg_copy.get("tool_calls"):
if (
msg_copy.get("role") == "assistant"
and msg_copy.get("content") is None and msg_copy.get("tool_calls")
):
msg_copy["content"] = ""
logger.debug(
f"Sanitized null content, tool_calls={len(msg_copy['tool_calls'])}"
-1
View File
@@ -21,7 +21,6 @@ from httpx import ASGITransport, AsyncClient
from src.main import app
# =============================================================================
# Command Line Options
# =============================================================================
+8 -4
View File
@@ -4,8 +4,9 @@ Tests for agent REST API endpoints.
Includes integration tests that verify real code paths work correctly
without over-mocking (only LLM calls are mocked).
"""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
from src.domains.agents.schemas import PermissionMode
@@ -312,9 +313,10 @@ class TestAgentMethodSignatures:
def test_task_agent_run_accepts_mode(self):
"""Verify TaskAgentImpl.run() accepts mode parameter."""
from src.domains.agents.task.agent import TaskAgentImpl
import inspect
from src.domains.agents.task.agent import TaskAgentImpl
sig = inspect.signature(TaskAgentImpl.run)
params = list(sig.parameters.keys())
@@ -325,9 +327,10 @@ class TestAgentMethodSignatures:
def test_task_agent_run_stream_accepts_mode(self):
"""Verify TaskAgentImpl.run_stream() accepts mode parameter."""
from src.domains.agents.task.agent import TaskAgentImpl
import inspect
from src.domains.agents.task.agent import TaskAgentImpl
sig = inspect.signature(TaskAgentImpl.run_stream)
params = list(sig.parameters.keys())
@@ -337,9 +340,10 @@ class TestAgentMethodSignatures:
def test_trace_span_signature(self):
"""Verify trace_span only accepts expected parameters."""
from src.shared.logging import trace_span
import inspect
from src.shared.logging import trace_span
sig = inspect.signature(trace_span.__init__)
params = list(sig.parameters.keys())
+13 -11
View File
@@ -3,15 +3,14 @@ Tests for conversations domain.
Tests conversation CRUD, context building, and API endpoints.
"""
import pytest
from uuid import uuid4
import pytest
from src.domains.conversations.models import Conversation, Message
from src.domains.conversations.schemas import (
CreateConversationRequest,
AddMessageRequest,
ConversationResponse,
MessageResponse,
CreateConversationRequest,
)
@@ -173,9 +172,10 @@ class TestConversationService:
@pytest.mark.anyio
async def test_context_prompt_no_history(self):
"""Test building context prompt with no history."""
from src.domains.conversations.service import ConversationService
from unittest.mock import MagicMock
from src.domains.conversations.service import ConversationService
# Create mock session
mock_session = MagicMock()
service = ConversationService(mock_session)
@@ -190,10 +190,11 @@ class TestConversationService:
@pytest.mark.anyio
async def test_context_prompt_with_history(self):
"""Test building context prompt with message history."""
from src.domains.conversations.service import ConversationService
from src.domains.conversations.models import Message
from unittest.mock import MagicMock
from src.domains.conversations.models import Message
from src.domains.conversations.service import ConversationService
mock_session = MagicMock()
service = ConversationService(mock_session)
@@ -221,10 +222,11 @@ class TestConversationService:
@pytest.mark.anyio
async def test_context_prompt_with_summary(self):
"""Test building context prompt with summary message."""
from src.domains.conversations.service import ConversationService
from src.domains.conversations.models import Message
from unittest.mock import MagicMock
from src.domains.conversations.models import Message
from src.domains.conversations.service import ConversationService
mock_session = MagicMock()
service = ConversationService(mock_session)
@@ -253,8 +255,8 @@ class TestSummarization:
def test_format_messages_for_summary(self):
"""Test formatting messages for summarization."""
from src.domains.conversations.summarize import format_messages_for_summary
from src.domains.conversations.models import Message
from src.domains.conversations.summarize import format_messages_for_summary
messages = [
Message(
@@ -276,8 +278,8 @@ class TestSummarization:
def test_format_messages_with_summary(self):
"""Test formatting messages that include a summary."""
from src.domains.conversations.summarize import format_messages_for_summary
from src.domains.conversations.models import Message
from src.domains.conversations.summarize import format_messages_for_summary
messages = [
Message(
+1 -1
View File
@@ -6,8 +6,8 @@ from pathlib import Path
import pytest
from src.domains.tools.gitignore import GitignoreFilter, filter_gitignored
from src.domains.tools.file.glob import GlobFilesTool
from src.domains.tools.gitignore import GitignoreFilter, filter_gitignored
from src.domains.tools.search.grep import GrepContentTool
+1 -1
View File
@@ -6,7 +6,7 @@ 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 plan_agent, PlanAgentImpl
from src.domains.agents.plan import PlanAgentImpl, plan_agent
class TestPlanAgentRegistration:
+4 -5
View File
@@ -1,17 +1,16 @@
"""
Tests for retry utilities.
"""
import pytest
from unittest.mock import AsyncMock, patch
import httpx
import pytest
from src.shared.retry import (
with_retry,
retry_async,
calculate_backoff,
is_retryable_exception,
is_retryable_http_status,
calculate_backoff,
retry_async,
with_retry,
)
+3 -3
View File
@@ -6,10 +6,10 @@ from pathlib import Path
import pytest
from src.domains.tools.file.read import ReadFileTool
from src.domains.tools.file.write import WriteFileTool
from src.domains.tools.file.edit import EditFileTool
from src.domains.tools.file.glob import GlobFilesTool
from src.domains.tools.file.read import ReadFileTool
from src.domains.tools.file.write import WriteFileTool
from src.domains.tools.shell.bash_full import BashTool
@@ -275,5 +275,5 @@ class TestResourceLimits:
assert result.success
# Should only return 5 files
lines = [l for l in result.data.strip().split("\n") if l]
lines = [line for line in result.data.strip().split("\n") if line]
assert len(lines) <= 5
+16 -27
View File
@@ -3,11 +3,11 @@ Tests for the Task agent.
Tests registration, API endpoints, tool access, and spawn_agent functionality.
"""
import pytest
from unittest.mock import AsyncMock, patch
from src.domains.agents.base import get_agent, list_agents
from src.domains.agents.task import task_agent, TaskAgentImpl
from src.domains.agents.task import TaskAgentImpl, task_agent
class TestTaskAgentRegistration:
@@ -90,39 +90,28 @@ class TestTaskAgentTools:
class TestSpawnAgentTool:
"""Tests for spawn_agent orchestration functionality."""
@pytest.mark.skip(
reason="never finished — the body built a mock context and then asserted "
"nothing, so it counted as a passing test while verifying nothing"
)
@pytest.mark.anyio
async def test_spawn_explore_agent(self):
"""Test spawning an explore agent."""
from src.domains.agents.task.tools import register_task_tools
from src.domains.agents.base import AgentContext
from pydantic_ai import Agent, RunContext
from unittest.mock import MagicMock
"""Spawning an explore agent should delegate to the explore agent.
# Create a mock context
ctx = MagicMock(spec=RunContext)
ctx.deps = AgentContext(
working_dir="/tmp",
allowed_paths=["/tmp"],
timeout_seconds=30
)
The scaffolding that used to sit here — a MagicMock RunContext, an
AgentContext with a /tmp working dir, and a patch of
src.domains.agents.base.get_agent — ran and then stopped at the comment
"For now, verify the explore agent would be called correctly". There was
no assertion, so it passed unconditionally.
# Mock the explore agent
with patch("src.domains.agents.base.get_agent") as mock_get_agent:
mock_explore = AsyncMock()
mock_explore.run = AsyncMock(return_value="Found 5 Python files")
mock_get_agent.return_value = mock_explore
# Import and call spawn_agent directly
from src.domains.agents.task import tools
# We need to test the actual tool function
# For now, verify the explore agent would be called correctly
Removed rather than tidied: ruff flagged its imports as unused, and
deleting those would have made the test look clean while leaving it
hollow. git history has the setup for whoever finishes this.
"""
@pytest.mark.anyio
async def test_spawn_unknown_agent_returns_error(self):
"""Test that spawning unknown agent type returns error."""
from src.domains.agents.base import AgentContext
from unittest.mock import MagicMock
from pydantic_ai import RunContext
# We can't easily test the tool directly, but we can verify
# the agent type validation logic
+1 -2
View File
@@ -1,9 +1,8 @@
"""
Tests for token counting utilities.
"""
import pytest
from src.shared.tokens import count_tokens, count_message_tokens, estimate_tokens
from src.shared.tokens import count_message_tokens, count_tokens, estimate_tokens
class TestTokenCounting:
+1 -1
View File
@@ -6,8 +6,8 @@ from pathlib import Path
import pytest
from src.domains.tools.file.read import ReadFileTool
from src.domains.tools.file.glob import GlobFilesTool
from src.domains.tools.file.read import ReadFileTool
from src.domains.tools.search.grep import GrepContentTool
from src.domains.tools.shell.bash import BashReadOnlyTool
+2 -1
View File
@@ -1,8 +1,9 @@
"""
Tests for WebSearchTool.
"""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
from src.domains.tools.search.web import WebSearchTool