style: apply ruff's automatic fixes and formatter

Mechanical only, and separated from the judgment calls that follow so the
reviewable changes are not buried in a 98-file whitespace diff.

227 automatic fixes: 60 blank lines carrying whitespace, 60 unsorted import
blocks, 34 Optional[X] to X | None, 28 unused imports, 16 deprecated typing
imports, 12 datetime.timezone.utc to datetime.UTC, and assorted smaller
modernisations. Then `ruff format` over src and tests: 98 files reformatted,
35 already conforming.

No file among the unused-import findings defines __all__ or is an __init__.py,
so nothing here removes a re-export.

`make test`: 658 passed, unchanged from HEAD.

Two things observed while verifying, neither addressed here:

`pytest tests/` cannot collect — tests/e2e/test_orchestration_e2e.py uses an
`e2e` marker that is not registered, and the config is strict about markers.
This fails identically at HEAD, so it predates this change; `make test` passes
because it ignores tests/e2e, tests/integration and tests/contracts.

test_tatlock_tool_call_logging_calculator is flaky. It failed once in a full run
with these changes and passed on the next, passes in isolation with them, and
fails in isolation at HEAD. It is order- or timing-dependent, not a regression
from this commit — established by running the full suite both ways rather than
by reasoning about which change could have caused it.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-11 17:25:18 +02:00
co-authored by Claude
parent 57fa6c13fc
commit 78066fab1b
103 changed files with 1601 additions and 1749 deletions
+3 -4
View File
@@ -2,9 +2,10 @@
Tests for Biographer capability registration.
"""
import pytest
from unittest.mock import MagicMock, patch
import pytest
from src.agents.biographer.capability import (
BIOGRAPHER_CAPABILITY,
get_biographer_capability,
@@ -73,9 +74,7 @@ class TestBiographerRegistration:
"src.agents.biographer.capability.get_household_registry",
return_value=mock_registry,
):
with patch(
"src.agents.biographer.capability.get_biographer_agent"
) as mock_get_agent:
with patch("src.agents.biographer.capability.get_biographer_agent") as mock_get_agent:
mock_agent = MagicMock()
mock_get_agent.return_value = mock_agent
+3 -4
View File
@@ -2,9 +2,10 @@
Tests for Housekeeper capability registration.
"""
import pytest
from unittest.mock import MagicMock, patch
import pytest
from src.agents.housekeeper.capability import (
HOUSEKEEPER_CAPABILITY,
get_housekeeper_capability,
@@ -74,9 +75,7 @@ class TestHousekeeperRegistration:
"src.agents.housekeeper.capability.get_household_registry",
return_value=mock_registry,
):
with patch(
"src.agents.housekeeper.capability.get_housekeeper_agent"
) as mock_get_agent:
with patch("src.agents.housekeeper.capability.get_housekeeper_agent") as mock_get_agent:
mock_agent = MagicMock()
mock_get_agent.return_value = mock_agent
+2 -1
View File
@@ -2,9 +2,10 @@
Tests for the Core-API HTTP client.
"""
import pytest
from unittest.mock import AsyncMock, MagicMock
import httpx
import pytest
from src.agents.housekeeper.client import (
Area,
+3 -4
View File
@@ -2,9 +2,10 @@
Tests for Librarian capability registration.
"""
import pytest
from unittest.mock import MagicMock, patch
import pytest
from src.agents.librarian.capability import (
LIBRARIAN_CAPABILITY,
get_librarian_capability,
@@ -67,9 +68,7 @@ class TestLibrarianRegistration:
"src.agents.librarian.capability.get_household_registry",
return_value=mock_registry,
):
with patch(
"src.agents.librarian.capability.get_librarian_agent"
) as mock_get_agent:
with patch("src.agents.librarian.capability.get_librarian_agent") as mock_get_agent:
mock_agent = MagicMock()
mock_get_agent.return_value = mock_agent
+8 -24
View File
@@ -137,9 +137,7 @@ class TestHybridSearch:
assert "docker" in result.keywords
@pytest.mark.asyncio
async def test_hybrid_search_empty_results(
self, client_with_mock, mock_httpx_client
):
async def test_hybrid_search_empty_results(self, client_with_mock, mock_httpx_client):
"""Test hybrid search with no results."""
mock_response = MagicMock()
mock_response.json.return_value = {
@@ -445,9 +443,7 @@ class TestUpdateWikiPage:
assert page.tags == ["projects", "devops"]
@pytest.mark.asyncio
async def test_update_wiki_page_multiple_fields(
self, client_with_mock, mock_httpx_client
):
async def test_update_wiki_page_multiple_fields(self, client_with_mock, mock_httpx_client):
"""Test updating multiple fields at once."""
mock_response = MagicMock()
mock_response.json.return_value = {
@@ -622,15 +618,11 @@ class TestExplicitUserContract:
@pytest.mark.asyncio
@pytest.mark.parametrize("method_name,kwargs", TENANT_SCOPED_METHODS)
async def test_user_from_context_is_sent_on_the_wire(
self, method_name, kwargs
):
async def test_user_from_context_is_sent_on_the_wire(self, method_name, kwargs):
"""With no explicit user, the context user is resolved and sent."""
client, mock_httpx = self._wire_client()
with patch(
"src.agents.librarian.client.get_user", return_value="llm_tester"
):
with patch("src.agents.librarian.client.get_user", return_value="llm_tester"):
await getattr(client, method_name)(**kwargs)
assert self._sent_user(mock_httpx) == "llm_tester"
@@ -647,9 +639,7 @@ class TestExplicitUserContract:
@pytest.mark.asyncio
@pytest.mark.parametrize("method_name,kwargs", TENANT_SCOPED_METHODS)
async def test_empty_context_user_fails_before_any_request(
self, method_name, kwargs
):
async def test_empty_context_user_fails_before_any_request(self, method_name, kwargs):
"""An empty resolved user raises before any bytes hit the wire."""
client, mock_httpx = self._wire_client()
@@ -698,9 +688,7 @@ class TestExplicitUserContract:
from src.core import config as config_module
from src.core.config import Environment
monkeypatch.setattr(
config_module.config, "ENVIRONMENT", Environment.DEVELOPMENT
)
monkeypatch.setattr(config_module.config, "ENVIRONMENT", Environment.DEVELOPMENT)
client, mock_httpx = self._wire_client()
await getattr(client, method_name)(user=explicit_user, **kwargs)
@@ -708,16 +696,12 @@ class TestExplicitUserContract:
assert self._sent_user(mock_httpx) == "llm_tester"
@pytest.mark.asyncio
async def test_explicit_production_tenant_passes_through_in_prod(
self, monkeypatch
):
async def test_explicit_production_tenant_passes_through_in_prod(self, monkeypatch):
"""In production the production tenant is sent unchanged."""
from src.core import config as config_module
from src.core.config import Environment
monkeypatch.setattr(
config_module.config, "ENVIRONMENT", Environment.PRODUCTION
)
monkeypatch.setattr(config_module.config, "ENVIRONMENT", Environment.PRODUCTION)
client, mock_httpx = self._wire_client()
await client.hybrid_search("q", user="jpmschweitzer")
+5 -15
View File
@@ -91,9 +91,7 @@ class TestBoundedRetries:
mock_httpx.post.side_effect = httpx.ConnectError("Connection refused")
with pytest.raises(httpx.ConnectError):
await client_with_mock.create_wiki_page(
title="T", path="/t", content="c", user="u"
)
await client_with_mock.create_wiki_page(title="T", path="/t", content="c", user="u")
assert mock_httpx.post.call_count == 1
@@ -103,9 +101,7 @@ class TestBoundedRetries:
mock_httpx.post.side_effect = httpx.ConnectError("Connection refused")
with pytest.raises(httpx.ConnectError):
await client_with_mock.smart_create_wiki_page(
topic="T", tags=["x"], user="u"
)
await client_with_mock.smart_create_wiki_page(topic="T", tags=["x"], user="u")
assert mock_httpx.post.call_count == 1
@@ -184,9 +180,7 @@ class TestModelRetryEscalation:
@pytest.mark.asyncio
async def test_read_tool_raises_model_retry_on_transport_error(self):
mock_client = AsyncMock()
mock_client.hybrid_search.side_effect = httpx.ConnectError(
"Connection refused"
)
mock_client.hybrid_search.side_effect = httpx.ConnectError("Connection refused")
with self._patched_client(mock_client):
with pytest.raises(ModelRetry):
@@ -219,14 +213,10 @@ class TestModelRetryEscalation:
@pytest.mark.asyncio
async def test_write_tool_never_raises_model_retry(self):
mock_client = AsyncMock()
mock_client.create_wiki_page.side_effect = httpx.ConnectError(
"Connection refused"
)
mock_client.create_wiki_page.side_effect = httpx.ConnectError("Connection refused")
with self._patched_client(mock_client):
result = await create_wiki_page(
title="T", path="/t", content="c", tags=["x"]
)
result = await create_wiki_page(title="T", path="/t", content="c", tags=["x"])
assert "unable" in result
assert "Connection refused" not in result
+12 -28
View File
@@ -83,9 +83,7 @@ class TestHybridRAGContract:
assert source in SOURCE_ICONS, f"no icon for source '{source}'"
@pytest.mark.asyncio
async def test_context_maps_to_formatted_context(
self, client_with_recorded_response
):
async def test_context_maps_to_formatted_context(self, client_with_recorded_response):
"""Top-level 'context' field maps to formatted_context."""
response = await client_with_recorded_response.hybrid_search(
"home server infrastructure", user="testuser"
@@ -94,9 +92,7 @@ class TestHybridRAGContract:
assert response.formatted_context != ""
@pytest.mark.asyncio
async def test_keywords_and_synonyms_from_dict(
self, client_with_recorded_response
):
async def test_keywords_and_synonyms_from_dict(self, client_with_recorded_response):
"""keywords is a dict: core_keywords + nested synonyms map."""
response = await client_with_recorded_response.hybrid_search(
"home server infrastructure", user="testuser"
@@ -124,9 +120,7 @@ class TestHybridRAGContract:
assert len(response.related_dossiers) == len(set(response.related_dossiers))
@pytest.mark.asyncio
async def test_payload_never_sends_zero_limits(
self, client_with_recorded_response
):
async def test_payload_never_sends_zero_limits(self, client_with_recorded_response):
"""The live service 422s on limits < 1; disabled legs use enable_* flags."""
await client_with_recorded_response.hybrid_search(
"home server infrastructure",
@@ -151,24 +145,18 @@ class TestHybridRAGContract:
assert config["enable_volatile"] is False
@pytest.mark.asyncio
async def test_user_always_sent_as_query_param(
self, client_with_recorded_response
):
async def test_user_always_sent_as_query_param(self, client_with_recorded_response):
"""The tenant is always sent explicitly - library-desk is removing
its server-side default, so a missing user would 422."""
await client_with_recorded_response.hybrid_search(
"home server infrastructure", user="testuser"
)
params = client_with_recorded_response._client.post.call_args.kwargs[
"params"
]
params = client_with_recorded_response._client.post.call_args.kwargs["params"]
assert params["user"] == "testuser"
@pytest.mark.asyncio
async def test_source_counts_and_timing_parsed(
self, client_with_recorded_response
):
async def test_source_counts_and_timing_parsed(self, client_with_recorded_response):
"""source_counts and timing map into the response model."""
response = await client_with_recorded_response.hybrid_search(
"home server infrastructure", user="testuser"
@@ -178,9 +166,7 @@ class TestHybridRAGContract:
assert response.timing.get("total_ms", 0) > 0
@pytest.mark.asyncio
async def test_source_status_absent_is_tolerated(
self, client_with_recorded_response
):
async def test_source_status_absent_is_tolerated(self, client_with_recorded_response):
"""Recorded response predates source_status/degraded - defaults apply."""
response = await client_with_recorded_response.hybrid_search(
"home server infrastructure", user="testuser"
@@ -218,7 +204,9 @@ class TestHybridRAGContract:
assert response.source_status["volatile"] == "disabled"
@pytest.mark.asyncio
async def test_tool_renders_no_unknown_results(self, client_with_recorded_response, monkeypatch):
async def test_tool_renders_no_unknown_results(
self, client_with_recorded_response, monkeypatch
):
"""The hybrid_search tool renders real sources and non-zero scores."""
class _Factory:
@@ -231,9 +219,7 @@ class TestHybridRAGContract:
async def __aexit__(self, *args):
return None
monkeypatch.setattr(
"src.agents.librarian.tools.LibraryDeskClient", _Factory()
)
monkeypatch.setattr("src.agents.librarian.tools.LibraryDeskClient", _Factory())
output = await hybrid_search("home server infrastructure")
@@ -278,9 +264,7 @@ class TestCoverageNote:
def test_wiki_leg_absence_is_not_degradation(self):
"""vector/graph missing from top-N counts is healthy ranking, not outage."""
response = self._response(
source_counts={"web": 2, "documents": 1, "volatile": 1}
)
response = self._response(source_counts={"web": 2, "documents": 1, "volatile": 1})
note = _coverage_note(
response, include_web=True, include_documents=True, include_volatile=True
)
+1 -3
View File
@@ -35,9 +35,7 @@ def _nullable_anyof_paths(schema: object, path: str = "") -> list[str]:
class TestLibrarianToolSchemas:
"""All registered librarian tools emit Ollama-safe parameter schemas."""
@pytest.mark.parametrize(
"tool_func", LIBRARIAN_TOOLS, ids=lambda f: f.__name__
)
@pytest.mark.parametrize("tool_func", LIBRARIAN_TOOLS, ids=lambda f: f.__name__)
def test_no_nullable_anyof_in_schema(self, tool_func):
schema = Tool(tool_func).function_schema.json_schema
+26 -35
View File
@@ -36,6 +36,7 @@ def mock_client():
# Web Search Tests
# ============================================================================
@pytest.mark.unit
class TestSearchWeb:
"""Tests for search_web tool."""
@@ -68,9 +69,7 @@ class TestSearchWeb:
)
mock_client.search_web.return_value = mock_response
with patch(
"src.agents.librarian.tools.LibraryDeskClient"
) as mock_client_class:
with patch("src.agents.librarian.tools.LibraryDeskClient") as mock_client_class:
mock_client_class.return_value.__aenter__.return_value = mock_client
mock_client_class.return_value.__aexit__.return_value = None
@@ -94,9 +93,7 @@ class TestSearchWeb:
)
mock_client.search_web.return_value = mock_response
with patch(
"src.agents.librarian.tools.LibraryDeskClient"
) as mock_client_class:
with patch("src.agents.librarian.tools.LibraryDeskClient") as mock_client_class:
mock_client_class.return_value.__aenter__.return_value = mock_client
mock_client_class.return_value.__aexit__.return_value = None
@@ -111,9 +108,7 @@ class TestSearchWeb:
"Connection failed to http://internal-host:8089"
)
with patch(
"src.agents.librarian.tools.LibraryDeskClient"
) as mock_client_class:
with patch("src.agents.librarian.tools.LibraryDeskClient") as mock_client_class:
mock_client_class.return_value.__aenter__.return_value = mock_client
mock_client_class.return_value.__aexit__.return_value = None
@@ -144,9 +139,7 @@ class TestSearchWeb:
)
mock_client.search_web.return_value = mock_response
with patch(
"src.agents.librarian.tools.LibraryDeskClient"
) as mock_client_class:
with patch("src.agents.librarian.tools.LibraryDeskClient") as mock_client_class:
mock_client_class.return_value.__aenter__.return_value = mock_client
mock_client_class.return_value.__aexit__.return_value = None
@@ -164,6 +157,7 @@ class TestSearchWeb:
# Read URL Tests
# ============================================================================
@pytest.mark.unit
class TestReadUrl:
"""Tests for read_url tool."""
@@ -182,9 +176,7 @@ class TestReadUrl:
)
mock_client.extract_content.return_value = mock_result
with patch(
"src.agents.librarian.tools.LibraryDeskClient"
) as mock_client_class:
with patch("src.agents.librarian.tools.LibraryDeskClient") as mock_client_class:
mock_client_class.return_value.__aenter__.return_value = mock_client
mock_client_class.return_value.__aexit__.return_value = None
@@ -205,9 +197,7 @@ class TestReadUrl:
)
mock_client.extract_content.return_value = mock_result
with patch(
"src.agents.librarian.tools.LibraryDeskClient"
) as mock_client_class:
with patch("src.agents.librarian.tools.LibraryDeskClient") as mock_client_class:
mock_client_class.return_value.__aenter__.return_value = mock_client
mock_client_class.return_value.__aexit__.return_value = None
@@ -227,9 +217,7 @@ class TestReadUrl:
)
mock_client.extract_content.return_value = mock_result
with patch(
"src.agents.librarian.tools.LibraryDeskClient"
) as mock_client_class:
with patch("src.agents.librarian.tools.LibraryDeskClient") as mock_client_class:
mock_client_class.return_value.__aenter__.return_value = mock_client
mock_client_class.return_value.__aexit__.return_value = None
@@ -246,6 +234,7 @@ class TestReadUrl:
# Batch URL Tests
# ============================================================================
@pytest.mark.unit
class TestReadUrlsBatch:
"""Tests for read_urls_batch tool."""
@@ -275,16 +264,16 @@ class TestReadUrlsBatch:
)
mock_client.extract_content_batch.return_value = mock_response
with patch(
"src.agents.librarian.tools.LibraryDeskClient"
) as mock_client_class:
with patch("src.agents.librarian.tools.LibraryDeskClient") as mock_client_class:
mock_client_class.return_value.__aenter__.return_value = mock_client
mock_client_class.return_value.__aexit__.return_value = None
result = await read_urls_batch([
"https://example.com/1",
"https://example.com/2",
])
result = await read_urls_batch(
[
"https://example.com/1",
"https://example.com/2",
]
)
assert "Article 1" in result
assert "Article 2" in result
@@ -314,16 +303,16 @@ class TestReadUrlsBatch:
)
mock_client.extract_content_batch.return_value = mock_response
with patch(
"src.agents.librarian.tools.LibraryDeskClient"
) as mock_client_class:
with patch("src.agents.librarian.tools.LibraryDeskClient") as mock_client_class:
mock_client_class.return_value.__aenter__.return_value = mock_client
mock_client_class.return_value.__aexit__.return_value = None
result = await read_urls_batch([
"https://example.com/good",
"https://example.com/bad",
])
result = await read_urls_batch(
[
"https://example.com/good",
"https://example.com/bad",
]
)
# Should contain successful result
assert "Good Article" in result
@@ -336,6 +325,7 @@ class TestReadUrlsBatch:
# Response Model Tests
# ============================================================================
@pytest.mark.unit
class TestWebSearchModels:
"""Tests for web search response models."""
@@ -436,6 +426,7 @@ class TestWebSearchModels:
# Wiki Update Tests (tag sentinel behavior)
# ============================================================================
@pytest.mark.unit
class TestUpdateWikiPageTagSentinels:
"""Empty list leaves tags unchanged; the clear sentinel empties them."""
+3 -4
View File
@@ -3,7 +3,6 @@ Tests for Steward schemas.
Tests the structured output models for conversation context and recommendations.
"""
import pytest
from src.agents.steward.schemas import ConversationContext, StewardRecommendation
@@ -24,7 +23,7 @@ class TestConversationContext:
context = ConversationContext(
has_previous_context=True,
relevant_turns=[0, 2, 4],
context_summary="User discussed weather in turns 0 and 2"
context_summary="User discussed weather in turns 0 and 2",
)
assert context.has_previous_context is True
@@ -89,7 +88,7 @@ class TestStewardRecommendation:
context = ConversationContext(
has_previous_context=True,
relevant_turns=[1, 3],
context_summary="User asked about calculation in turn 1, now wants explanation"
context_summary="User asked about calculation in turn 1, now wants explanation",
)
rec = StewardRecommendation(
@@ -122,7 +121,7 @@ class TestStewardRecommendation:
context = ConversationContext(
has_previous_context=True,
relevant_turns=[0],
context_summary="Previous calculation mentioned"
context_summary="Previous calculation mentioned",
)
rec = StewardRecommendation(
+17 -24
View File
@@ -3,6 +3,7 @@ Tests for Steward service layer.
Tests request analysis, logging, and benchmarking integration.
"""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@@ -31,7 +32,9 @@ class TestAnalyzeRequest:
"""Test analyzing a simple greeting."""
# Mock the Steward agent's analyze method (plain text approach)
mock_agent = MagicMock()
mock_agent.analyze = AsyncMock(return_value="Simple greeting requires no tools. This is a simple request.")
mock_agent.analyze = AsyncMock(
return_value="Simple greeting requires no tools. This is a simple request."
)
with patch("src.agents.steward.service.get_steward_agent", return_value=mock_agent):
result = await analyze_request(
@@ -159,7 +162,7 @@ class TestFormatStewardNote:
context = ConversationContext(
has_previous_context=True,
relevant_turns=[0, 1],
context_summary="Previous discussion about calculations"
context_summary="Previous discussion about calculations",
)
rec = StewardRecommendation(
@@ -205,9 +208,7 @@ class TestBuildEnrichedQuery:
def test_enrichment_adds_location(self):
"""Test location is appended for weather queries."""
query = "What's the weather?"
memory_context = {
"profile": {"location": "Amsterdam", "timezone": "Europe/Amsterdam"}
}
memory_context = {"profile": {"location": "Amsterdam", "timezone": "Europe/Amsterdam"}}
result = _build_enriched_query(query, memory_context)
@@ -218,9 +219,7 @@ class TestBuildEnrichedQuery:
def test_no_location_when_specified(self):
"""Test location is not appended when already specified."""
query = "What's the weather in London?"
memory_context = {
"profile": {"location": "Amsterdam"}
}
memory_context = {"profile": {"location": "Amsterdam"}}
result = _build_enriched_query(query, memory_context)
@@ -230,9 +229,7 @@ class TestBuildEnrichedQuery:
def test_enrichment_adds_timezone(self):
"""Test timezone is appended for time queries."""
query = "What time is it?"
memory_context = {
"profile": {"timezone": "Europe/Amsterdam"}
}
memory_context = {"profile": {"timezone": "Europe/Amsterdam"}}
result = _build_enriched_query(query, memory_context)
@@ -241,9 +238,7 @@ class TestBuildEnrichedQuery:
def test_no_timezone_when_specified(self):
"""Test timezone is not appended when already specified."""
query = "What time is it in UTC?"
memory_context = {
"profile": {"timezone": "Europe/Amsterdam"}
}
memory_context = {"profile": {"timezone": "Europe/Amsterdam"}}
result = _build_enriched_query(query, memory_context)
@@ -254,7 +249,7 @@ class TestBuildEnrichedQuery:
query = "What's the weather?"
memory_context = {
"profile": {"location": "Amsterdam"},
"preferences": {"temperature_unit": "celsius"}
"preferences": {"temperature_unit": "celsius"},
}
result = _build_enriched_query(query, memory_context)
@@ -265,11 +260,8 @@ class TestBuildEnrichedQuery:
"""Test multiple context fields are appended."""
query = "What time and weather today?"
memory_context = {
"profile": {
"location": "Amsterdam",
"timezone": "Europe/Amsterdam"
},
"preferences": {"temperature_unit": "celsius"}
"profile": {"location": "Amsterdam", "timezone": "Europe/Amsterdam"},
"preferences": {"temperature_unit": "celsius"},
}
result = _build_enriched_query(query, memory_context)
@@ -281,9 +273,7 @@ class TestBuildEnrichedQuery:
def test_no_enrichment_for_unrelated_query(self):
"""Test no enrichment for queries that don't need context."""
query = "Tell me a joke"
memory_context = {
"profile": {"location": "Amsterdam", "timezone": "Europe/Amsterdam"}
}
memory_context = {"profile": {"location": "Amsterdam", "timezone": "Europe/Amsterdam"}}
result = _build_enriched_query(query, memory_context)
@@ -306,7 +296,10 @@ class TestExtractCapabilities:
("The user wants a description of the algorithm.", "script -> housekeeper"),
("I should discover what the answer is.", "cover -> housekeeper"),
("That sounds fantastic, let me compute it.", "fan -> housekeeper"),
("I acknowledge the request to add two numbers.", "knowledge/know -> librarian, biographer"),
(
"I acknowledge the request to add two numbers.",
"knowledge/know -> librarian, biographer",
),
("The user asks about the economy myth.", "my -> biographer"),
("Convert 98.6 Fahrenheit to Celsius.", "temperature is a housekeeper domain"),
]
+27 -13
View File
@@ -4,6 +4,7 @@ Tests for delegation infrastructure.
Tests the DelegationTask dataclass and delegation wrapper functions
that implement the agent-as-tool pattern.
"""
from unittest.mock import AsyncMock, patch
import pytest
@@ -41,9 +42,7 @@ class TestBuildDelegationContext:
assert "assistant: Docker is a container runtime." in context
def test_only_last_max_turns_kept(self):
history = [
{"role": "user", "content": f"message {i}"} for i in range(10)
]
history = [{"role": "user", "content": f"message {i}"} for i in range(10)]
context = build_delegation_context(history, max_turns=6)
@@ -60,9 +59,7 @@ class TestBuildDelegationContext:
assert "x" * 501 not in context
def test_structured_content_parts_tolerated(self):
history = [
{"role": "user", "content": [{"type": "text", "text": "hello there"}]}
]
history = [{"role": "user", "content": [{"type": "text", "text": "hello there"}]}]
context = build_delegation_context(history)
@@ -343,8 +340,12 @@ class TestHouseholdThinkMessages:
for action_type, messages in action_types.items():
for phase, msg in messages.items():
# Messages should NOT have <think> wrappers - they go to reasoning_content field
assert "<think>" not in msg, f"{expert}/{action_type}/{phase} should not have <think> wrapper"
assert "</think>" not in msg, f"{expert}/{action_type}/{phase} should not have </think> wrapper"
assert (
"<think>" not in msg
), f"{expert}/{action_type}/{phase} should not have <think> wrapper"
assert (
"</think>" not in msg
), f"{expert}/{action_type}/{phase} should not have </think> wrapper"
# Messages should be non-empty strings
assert isinstance(msg, str) and len(msg) > 0, f"{expert}/{action_type}/{phase}"
@@ -356,7 +357,9 @@ class TestDetectActionType:
def test_librarian_search_is_retrieve(self):
"""Test librarian search tasks are RETRIEVE."""
assert _detect_action_type("librarian", "search for Docker info") == ActionType.RETRIEVE
assert _detect_action_type("librarian", "find information about CI/CD") == ActionType.RETRIEVE
assert (
_detect_action_type("librarian", "find information about CI/CD") == ActionType.RETRIEVE
)
assert _detect_action_type("librarian", "look up Kubernetes docs") == ActionType.RETRIEVE
def test_librarian_web_search_is_research(self):
@@ -378,14 +381,25 @@ class TestDetectActionType:
def test_biographer_record_is_record(self):
"""Test biographer record tasks are RECORD."""
assert _detect_action_type("biographer", "remember that I work at Acme") == ActionType.RECORD
assert (
_detect_action_type("biographer", "remember that I work at Acme") == ActionType.RECORD
)
assert _detect_action_type("biographer", "note that my car is a Tesla") == ActionType.RECORD
assert _detect_action_type("biographer", "save my preference for dark mode") == ActionType.RECORD
assert (
_detect_action_type("biographer", "save my preference for dark mode")
== ActionType.RECORD
)
def test_housekeeper_status_is_retrieve(self):
"""Test housekeeper status tasks are RETRIEVE."""
assert _detect_action_type("housekeeper", "what devices are in the bedroom?") == ActionType.RETRIEVE
assert _detect_action_type("housekeeper", "is the living room light on?") == ActionType.RETRIEVE
assert (
_detect_action_type("housekeeper", "what devices are in the bedroom?")
== ActionType.RETRIEVE
)
assert (
_detect_action_type("housekeeper", "is the living room light on?")
== ActionType.RETRIEVE
)
def test_housekeeper_control_is_control(self):
"""Test housekeeper control tasks are CONTROL."""
+2 -2
View File
@@ -6,9 +6,9 @@ import pytest
from src.agents.lorem_tester import LoremTesterAgent
from src.core.exceptions import (
RateLimitError,
ContextLengthError,
APIError,
ContextLengthError,
RateLimitError,
)
+67 -42
View File
@@ -5,23 +5,25 @@ Tests the multi-expert coordination infrastructure including
delegation parsing, think updates, result handling, and
multi-expert sequential/parallel execution.
"""
import pytest
from unittest.mock import AsyncMock, patch
import pytest
from src.agents.delegation import DelegationResult, DelegationTask
from src.agents.orchestration import (
OrchestrationContext,
parse_delegation_from_steward_note,
execute_delegation,
orchestrate_with_think_updates,
extract_delegation_context,
ExecutionMode,
MultiExpertResult,
execute_sequential,
execute_parallel,
orchestrate_multi_expert,
OrchestrationContext,
_get_display_name,
execute_delegation,
execute_parallel,
execute_sequential,
extract_delegation_context,
orchestrate_multi_expert,
orchestrate_with_think_updates,
parse_delegation_from_steward_note,
)
from src.agents.delegation import DelegationTask, DelegationResult
@pytest.mark.unit
@@ -364,6 +366,7 @@ class TestOrchestrationContext:
# Multi-Expert Coordination Tests
# ============================================================================
@pytest.mark.unit
class TestMultiExpertResult:
"""Tests for MultiExpertResult aggregation."""
@@ -414,18 +417,22 @@ class TestMultiExpertResult:
"""Test aggregating outputs from multiple experts."""
result = MultiExpertResult()
result.add_result(DelegationResult(
expert_name="librarian",
task="search docs",
success=True,
output="Found Docker docs",
))
result.add_result(DelegationResult(
expert_name="memory",
task="get preferences",
success=True,
output="User prefers dark mode",
))
result.add_result(
DelegationResult(
expert_name="librarian",
task="search docs",
success=True,
output="Found Docker docs",
)
)
result.add_result(
DelegationResult(
expert_name="memory",
task="get preferences",
success=True,
output="User prefers dark mode",
)
)
combined = result.aggregate_outputs()
@@ -438,19 +445,23 @@ class TestMultiExpertResult:
"""Test that failed results are excluded from aggregate."""
result = MultiExpertResult()
result.add_result(DelegationResult(
expert_name="librarian",
task="search",
success=True,
output="Success output",
))
result.add_result(DelegationResult(
expert_name="memory",
task="get",
success=False,
output="",
error="Failed",
))
result.add_result(
DelegationResult(
expert_name="librarian",
task="search",
success=True,
output="Success output",
)
)
result.add_result(
DelegationResult(
expert_name="memory",
task="get",
success=False,
output="",
error="Failed",
)
)
combined = result.aggregate_outputs()
@@ -471,7 +482,9 @@ class TestExecuteSequential:
]
mock_results = [
DelegationResult(expert_name="librarian", task="task 1", success=True, output="Result 1"),
DelegationResult(
expert_name="librarian", task="task 1", success=True, output="Result 1"
),
DelegationResult(expert_name="memory", task="task 2", success=True, output="Result 2"),
]
@@ -496,7 +509,9 @@ class TestExecuteSequential:
mock_results = [
DelegationResult(expert_name="librarian", task="task 1", success=True, output="OK"),
DelegationResult(expert_name="memory", task="task 2", success=False, output="", error="Failed"),
DelegationResult(
expert_name="memory", task="task 2", success=False, output="", error="Failed"
),
]
with patch(
@@ -520,7 +535,9 @@ class TestExecuteSequential:
]
mock_results = [
DelegationResult(expert_name="librarian", task="task 1", success=False, output="", error="Error"),
DelegationResult(
expert_name="librarian", task="task 1", success=False, output="", error="Error"
),
]
with patch(
@@ -548,7 +565,9 @@ class TestExecuteParallel:
]
mock_results = [
DelegationResult(expert_name="librarian", task="task 1", success=True, output="Result 1"),
DelegationResult(
expert_name="librarian", task="task 1", success=True, output="Result 1"
),
DelegationResult(expert_name="memory", task="task 2", success=True, output="Result 2"),
]
@@ -572,7 +591,9 @@ class TestExecuteParallel:
mock_results = [
DelegationResult(expert_name="librarian", task="task 1", success=True, output="OK"),
DelegationResult(expert_name="memory", task="task 2", success=False, output="", error="Timeout"),
DelegationResult(
expert_name="memory", task="task 2", success=False, output="", error="Timeout"
),
]
with patch(
@@ -629,7 +650,9 @@ class TestOrchestrateMultiExpert:
]
mock_results = [
DelegationResult(expert_name="librarian", task="task 1", success=True, output="Result 1"),
DelegationResult(
expert_name="librarian", task="task 1", success=True, output="Result 1"
),
DelegationResult(expert_name="memory", task="task 2", success=True, output="Result 2"),
]
@@ -658,7 +681,9 @@ class TestOrchestrateMultiExpert:
]
mock_results = [
DelegationResult(expert_name="librarian", task="task 1", success=True, output="Result 1"),
DelegationResult(
expert_name="librarian", task="task 1", success=True, output="Result 1"
),
DelegationResult(expert_name="memory", task="task 2", success=True, output="Result 2"),
]
+1
View File
@@ -1,6 +1,7 @@
"""
Tests for the agent error protocol.
"""
import pytest
from src.agents.protocol import AgentError
+5 -5
View File
@@ -4,8 +4,8 @@ Tests for model registry.
import pytest
from src.agents.registry import ModelRegistry
from src.agents.lorem_tester import LoremTesterAgent
from src.agents.registry import ModelRegistry
from src.agents.tatlock import TatlockAgent
from src.core.exceptions import ModelNotFoundError
@@ -63,10 +63,10 @@ async def test_tatlock_capabilities():
# Tatlock Phase 1 - basic streaming, reasoning, and permanent tools
assert capabilities["streaming"] is True
assert capabilities["reasoning"] is True # Basic reasoning summaries
assert capabilities["tools"] is True # Permanent tools: calculator, date/time, search
assert capabilities["vision"] is False # Future
assert capabilities["audio"] is False # Future
assert capabilities["reasoning"] is True # Basic reasoning summaries
assert capabilities["tools"] is True # Permanent tools: calculator, date/time, search
assert capabilities["vision"] is False # Future
assert capabilities["audio"] is False # Future
@pytest.mark.unit
+94 -132
View File
@@ -6,9 +6,7 @@ These tests verify:
2. Tool calls are logged to reasoning output (users see what tools are doing)
"""
import json
import pytest
from unittest.mock import patch, AsyncMock
from httpx import AsyncClient
@@ -28,14 +26,10 @@ async def test_tatlock_conversation_history_memory(async_client: AsyncClient):
"messages": [
{"role": "user", "content": "My name is Alice and I love Python programming."}
],
"stream": False
"stream": False,
}
response_1 = await async_client.post(
"/v1/chat/completions",
json=request_data_1,
timeout=120.0
)
response_1 = await async_client.post("/v1/chat/completions", json=request_data_1, timeout=120.0)
assert response_1.status_code == 200
data_1 = response_1.json()
@@ -48,16 +42,15 @@ async def test_tatlock_conversation_history_memory(async_client: AsyncClient):
"messages": [
{"role": "user", "content": "My name is Alice and I love Python programming."},
{"role": "assistant", "content": first_response},
{"role": "user", "content": "What did I say my name was? And what programming language did I mention?"}
{
"role": "user",
"content": "What did I say my name was? And what programming language did I mention?",
},
],
"stream": False
"stream": False,
}
response_2 = await async_client.post(
"/v1/chat/completions",
json=request_data_2,
timeout=120.0
)
response_2 = await async_client.post("/v1/chat/completions", json=request_data_2, timeout=120.0)
assert response_2.status_code == 200
data_2 = response_2.json()
@@ -68,7 +61,9 @@ async def test_tatlock_conversation_history_memory(async_client: AsyncClient):
has_python = "python" in second_response
if not has_alice or not has_python:
pytest.xfail(f"LLM did not remember context (non-deterministic): alice={has_alice}, python={has_python}, response: {second_response[:200]}")
pytest.xfail(
f"LLM did not remember context (non-deterministic): alice={has_alice}, python={has_python}, response: {second_response[:200]}"
)
@pytest.mark.integration
@@ -86,48 +81,37 @@ async def test_tatlock_multi_turn_context(async_client: AsyncClient):
# Turn 1: Set up a topic
conversation.append({"role": "user", "content": "Let's talk about the number 42."})
request_1 = {
"model": "Tatlock",
"messages": conversation.copy(),
"stream": False
}
request_1 = {"model": "Tatlock", "messages": conversation.copy(), "stream": False}
response_1 = await async_client.post(
"/v1/chat/completions",
json=request_1,
timeout=120.0
)
response_1 = await async_client.post("/v1/chat/completions", json=request_1, timeout=120.0)
assert response_1.status_code == 200
data_1 = response_1.json()
conversation.append({
"role": "assistant",
"content": data_1["choices"][0]["message"]["content"]
})
conversation.append(
{"role": "assistant", "content": data_1["choices"][0]["message"]["content"]}
)
# Turn 2: Reference "it" (should refer to 42)
conversation.append({"role": "user", "content": "What number did I just mention?"})
request_2 = {
"model": "Tatlock",
"messages": conversation.copy(),
"stream": False
}
request_2 = {"model": "Tatlock", "messages": conversation.copy(), "stream": False}
response_2 = await async_client.post(
"/v1/chat/completions",
json=request_2,
timeout=120.0
)
response_2 = await async_client.post("/v1/chat/completions", json=request_2, timeout=120.0)
assert response_2.status_code == 200
data_2 = response_2.json()
final_response = data_2["choices"][0]["message"]["content"]
# Should reference 42 (check both as digit and word)
has_42 = "42" in final_response or "forty-two" in final_response.lower() or "forty two" in final_response.lower()
has_42 = (
"42" in final_response
or "forty-two" in final_response.lower()
or "forty two" in final_response.lower()
)
if not has_42:
pytest.xfail(f"LLM did not mention 42 in response (non-deterministic): {final_response[:200]}")
pytest.xfail(
f"LLM did not mention 42 in response (non-deterministic): {final_response[:200]}"
)
@pytest.mark.integration
@@ -142,16 +126,15 @@ async def test_tatlock_tool_call_logging_search(async_client: AsyncClient):
request_data = {
"model": "Tatlock",
"messages": [
{"role": "user", "content": "Search for current information about Python 3.13 release date"}
{
"role": "user",
"content": "Search for current information about Python 3.13 release date",
}
],
"stream": False
"stream": False,
}
response = await async_client.post(
"/v1/chat/completions",
json=request_data,
timeout=120.0
)
response = await async_client.post("/v1/chat/completions", json=request_data, timeout=120.0)
assert response.status_code == 200
data = response.json()
@@ -167,8 +150,9 @@ async def test_tatlock_tool_call_logging_search(async_client: AsyncClient):
# If search was used, should show the 🔍 emoji
if "🔍" in full_response:
assert "search" in full_response.lower() or "python" in full_response.lower(), \
"Search query should be visible in the response"
assert (
"search" in full_response.lower() or "python" in full_response.lower()
), "Search query should be visible in the response"
@pytest.mark.integration
@@ -183,45 +167,38 @@ async def test_tatlock_tool_call_logging_calculator(async_client: AsyncClient):
"""
request_data = {
"model": "Tatlock",
"messages": [
{"role": "user", "content": "What is the square root of 144 plus 25?"}
],
"stream": False
"messages": [{"role": "user", "content": "What is the square root of 144 plus 25?"}],
"stream": False,
}
response = await async_client.post(
"/v1/chat/completions",
json=request_data,
timeout=120.0
)
response = await async_client.post("/v1/chat/completions", json=request_data, timeout=120.0)
assert response.status_code == 200
data = response.json()
full_response = data["choices"][0]["message"]["content"]
# Should have reasoning in <think> tags (from Steward analysis)
assert "<think>" in full_response, \
f"Should have reasoning output in <think> tags. Got: {full_response}"
assert (
"<think>" in full_response
), f"Should have reasoning output in <think> tags. Got: {full_response}"
# Should reference the calculation in some form
has_calculation_reference = (
"144" in full_response or
"sqrt" in full_response.lower() or
"square root" in full_response.lower()
"144" in full_response
or "sqrt" in full_response.lower()
or "square root" in full_response.lower()
)
assert has_calculation_reference, \
f"Should reference the calculation. Got: {full_response}"
assert has_calculation_reference, f"Should reference the calculation. Got: {full_response}"
# Should have the correct answer (37)
assert "37" in full_response, \
f"Should contain the answer 37. Got: {full_response}"
assert "37" in full_response, f"Should contain the answer 37. Got: {full_response}"
# Tool emoji is optional - depends on whether tool was used directly
# or computation was delegated to capability
if "🧮" in full_response:
print(f"\nCalculator tool was used directly")
print("\nCalculator tool was used directly")
else:
print(f"\nCalculation handled via tatlock_core capability")
print("\nCalculation handled via tatlock_core capability")
print(f"\nCalculator response: {full_response}")
@@ -234,17 +211,11 @@ async def test_tatlock_tool_call_logging_datetime(async_client: AsyncClient):
"""
request_data = {
"model": "Tatlock",
"messages": [
{"role": "user", "content": "What was the date exactly 2 weeks ago?"}
],
"stream": False
"messages": [{"role": "user", "content": "What was the date exactly 2 weeks ago?"}],
"stream": False,
}
response = await async_client.post(
"/v1/chat/completions",
json=request_data,
timeout=120.0
)
response = await async_client.post("/v1/chat/completions", json=request_data, timeout=120.0)
assert response.status_code == 200
data = response.json()
@@ -257,19 +228,36 @@ async def test_tatlock_tool_call_logging_datetime(async_client: AsyncClient):
used_date_tool = "🕐" in full_response
# Should mention the calculation or the timeframe
assert "2 weeks ago" in full_response.lower() or "weeks" in full_response.lower(), \
f"Should reference the requested timeframe. Got: {full_response}"
assert (
"2 weeks ago" in full_response.lower() or "weeks" in full_response.lower()
), f"Should reference the requested timeframe. Got: {full_response}"
# Should provide a specific date (either YYYY-MM-DD format or natural language like "November 23")
import re
has_iso_date = bool(re.search(r'\d{4}-\d{2}-\d{2}', full_response))
has_month_mention = any(month in full_response.lower() for month in
['january', 'february', 'march', 'april', 'may', 'june',
'july', 'august', 'september', 'october', 'november', 'december'])
has_date_number = bool(re.search(r'\b\d{1,2}(st|nd|rd|th)?\b', full_response.lower()))
assert has_iso_date or has_month_mention or has_date_number, \
f"Should contain a specific date. Got: {full_response}"
has_iso_date = bool(re.search(r"\d{4}-\d{2}-\d{2}", full_response))
has_month_mention = any(
month in full_response.lower()
for month in [
"january",
"february",
"march",
"april",
"may",
"june",
"july",
"august",
"september",
"october",
"november",
"december",
]
)
has_date_number = bool(re.search(r"\b\d{1,2}(st|nd|rd|th)?\b", full_response.lower()))
assert (
has_iso_date or has_month_mention or has_date_number
), f"Should contain a specific date. Got: {full_response}"
print(f"\nDate/time response (tool used: {used_date_tool}): {full_response}")
@@ -284,17 +272,11 @@ async def test_tatlock_no_tool_calls_no_logging(async_client: AsyncClient):
"""
request_data = {
"model": "Tatlock",
"messages": [
{"role": "user", "content": "Just say hello to me."}
],
"stream": False
"messages": [{"role": "user", "content": "Just say hello to me."}],
"stream": False,
}
response = await async_client.post(
"/v1/chat/completions",
json=request_data,
timeout=120.0
)
response = await async_client.post("/v1/chat/completions", json=request_data, timeout=120.0)
assert response.status_code == 200
data = response.json()
@@ -327,17 +309,9 @@ async def test_tatlock_conversation_history_with_tools(async_client: AsyncClient
# Turn 1: Do a calculation
conversation.append({"role": "user", "content": "Calculate 15 times 7 for me."})
request_1 = {
"model": "Tatlock",
"messages": conversation.copy(),
"stream": False
}
request_1 = {"model": "Tatlock", "messages": conversation.copy(), "stream": False}
response_1 = await async_client.post(
"/v1/chat/completions",
json=request_1,
timeout=120.0
)
response_1 = await async_client.post("/v1/chat/completions", json=request_1, timeout=120.0)
assert response_1.status_code == 200
data_1 = response_1.json()
@@ -353,17 +327,9 @@ async def test_tatlock_conversation_history_with_tools(async_client: AsyncClient
# Turn 2: Ask about previous calculation
conversation.append({"role": "user", "content": "What calculation did I just ask you to do?"})
request_2 = {
"model": "Tatlock",
"messages": conversation.copy(),
"stream": False
}
request_2 = {"model": "Tatlock", "messages": conversation.copy(), "stream": False}
response_2 = await async_client.post(
"/v1/chat/completions",
json=request_2,
timeout=120.0
)
response_2 = await async_client.post("/v1/chat/completions", json=request_2, timeout=120.0)
assert response_2.status_code == 200
data_2 = response_2.json()
@@ -371,13 +337,15 @@ async def test_tatlock_conversation_history_with_tools(async_client: AsyncClient
# Should remember the calculation (either as digits or words)
has_calculation = (
("15" in second_response and "7" in second_response) or # As digits
("fifteen" in second_response and "seven" in second_response) or # As words
"105" in second_response or # As answer
"multipl" in second_response # Mentions multiplication
("15" in second_response and "7" in second_response) # As digits
or ("fifteen" in second_response and "seven" in second_response) # As words
or "105" in second_response # As answer
or "multipl" in second_response # Mentions multiplication
)
if not has_calculation:
pytest.xfail(f"LLM did not remember calculation (non-deterministic): {second_response[:200]}")
pytest.xfail(
f"LLM did not remember calculation (non-deterministic): {second_response[:200]}"
)
@pytest.mark.integration
@@ -404,19 +372,13 @@ async def test_tatlock_ollama_fallback(async_client: AsyncClient):
request_data = {
"model": "Tatlock",
"messages": [
{"role": "user", "content": "Say hello to me."}
],
"stream": False
"messages": [{"role": "user", "content": "Say hello to me."}],
"stream": False,
}
# 300s: this test forbids the Claude rescue, and the full local
# Steward -> orchestrate -> synthesize flow on gemma4 exceeds 120s
response = await async_client.post(
"/v1/chat/completions",
json=request_data,
timeout=300.0
)
response = await async_client.post("/v1/chat/completions", json=request_data, timeout=300.0)
assert response.status_code == 200
data = response.json()
+3 -3
View File
@@ -5,21 +5,20 @@ Note: Web search has been moved to The Librarian agent.
See tests/agents/librarian/test_tools.py for search tests.
"""
import pytest
from datetime import datetime
from src.agents.tools import (
calculate,
get_current_datetime,
calculate_time_offset,
get_current_datetime,
time_difference,
)
# ============================================================================
# Calculator Tests
# ============================================================================
class TestCalculator:
"""Tests for the calculator tool."""
@@ -81,6 +80,7 @@ class TestCalculator:
# Date/Time Tests
# ============================================================================
class TestDateTime:
"""Tests for date/time toolkit."""