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:
@@ -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
|
||||
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,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(
|
||||
|
||||
@@ -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"),
|
||||
]
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -6,9 +6,9 @@ import pytest
|
||||
|
||||
from src.agents.lorem_tester import LoremTesterAgent
|
||||
from src.core.exceptions import (
|
||||
RateLimitError,
|
||||
ContextLengthError,
|
||||
APIError,
|
||||
ContextLengthError,
|
||||
RateLimitError,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
"""
|
||||
Tests for the agent error protocol.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from src.agents.protocol import AgentError
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user