honcho/tests/deriver/test_deriver_processing.py

186 lines
6.8 KiB
Python

import signal
from collections.abc import Callable, Generator
from datetime import datetime, timedelta, timezone
from typing import Any
from unittest.mock import AsyncMock
import pytest
from src import models
from src.deriver.deriver import process_representation_tasks_batch
from src.utils.representation import Representation
from src.utils.work_unit import construct_work_unit_key
@pytest.mark.asyncio
class TestDeriverProcessing:
"""Test suite for deriver processing using the conftest fixtures"""
async def test_mock_critical_analysis_call(
self,
mock_critical_analysis_call: Generator[Callable[..., Any], None, None],
sample_messages: list[models.Message],
):
"""Test that the critical analysis call is properly mocked"""
assert mock_critical_analysis_call is not None
assert len(sample_messages) > 0 # Verify we have messages for testing
# The mock should be in place and return a predefined response
# This ensures no actual LLM calls are made during testing
async def test_work_unit_key_generation(
self,
sample_session_with_peers: tuple[models.Session, list[models.Peer]],
):
"""Test that work unit keys are generated correctly"""
session, peers = sample_session_with_peers
peer1, peer2, _ = peers
# Create a payload for representation task
representation_payload = {
"session_name": session.name,
"observer": peer2.name,
"observed": peer1.name,
"task_type": "representation",
}
# Generate work unit key for representation
work_unit_key = construct_work_unit_key(
session.workspace_name, representation_payload
)
expected_key = f"representation:{session.workspace_name}:{session.name}:{peer2.name}:{peer1.name}"
assert work_unit_key == expected_key
# Create a payload for summary task
summary_payload = {
"session_name": session.name,
"task_type": "summary",
}
# Generate work unit key for summary
summary_work_unit_key = construct_work_unit_key(
session.workspace_name, summary_payload
)
expected_summary_key = (
f"summary:{session.workspace_name}:{session.name}:None:None"
)
assert summary_work_unit_key == expected_summary_key
async def test_mock_queue_manager(
self,
mock_queue_manager: Any, # AsyncMock object
sample_session_with_peers: tuple[models.Session, list[models.Peer]],
):
"""Test that the queue manager is properly mocked"""
session, peers = sample_session_with_peers
assert session is not None
assert len(peers) == 3
# Verify the mock has the expected attributes
assert mock_queue_manager is not None
assert hasattr(mock_queue_manager, "initialize")
assert hasattr(mock_queue_manager, "shutdown")
assert hasattr(mock_queue_manager, "process_work_unit")
# Verify we can call the mocked methods
await mock_queue_manager.initialize()
await mock_queue_manager.shutdown(signal.SIGTERM)
# Verify the mocked methods were called
mock_queue_manager.initialize.assert_called_once() # type: ignore[attr-defined]
mock_queue_manager.shutdown.assert_called_once() # type: ignore[attr-defined]
async def test_mock_representation_manager(
self,
mock_representation_manager: Any, # AsyncMock object
):
"""Test that the representation manager is properly mocked"""
assert mock_representation_manager is not None
# Verify we can call the mocked methods
await mock_representation_manager.save_representation(
Representation(explicit=[], deductive=[])
)
mock_representation_manager.get_relevant_observations.return_value = [] # type: ignore[attr-defined]
# Verify the methods were called
assert mock_representation_manager.save_representation.called # type: ignore[attr-defined]
async def test_representation_batch_uses_earliest_cutoff(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Ensure batching history cutoff uses the earliest payload in the batch."""
captured_cutoffs: list[int] = []
async def fake_get_session_context_formatted(*_args: Any, **kwargs: Any) -> str:
captured_cutoffs.append(kwargs["cutoff"])
return "formatted-history"
# Mock only the function we need to inspect for the test assertion
monkeypatch.setattr(
"src.deriver.deriver.summarizer.get_session_context_formatted",
fake_get_session_context_formatted,
)
# Provide a stub working representation so embedding lookups are skipped.
monkeypatch.setattr(
"src.crud.get_working_representation",
AsyncMock(
return_value=Representation(
explicit=[],
deductive=[],
)
),
)
# Avoid DB access for collection and peer card
monkeypatch.setattr(
"src.crud.get_or_create_collection",
AsyncMock(return_value=type("Collection", (), {"name": "dummy"})()),
)
monkeypatch.setattr(
"src.crud.get_peer_card",
AsyncMock(return_value=[]),
)
# Short-circuit tracked_db context manager
from contextlib import asynccontextmanager
@asynccontextmanager
async def _no_db(_label: str):
yield object()
monkeypatch.setattr("src.deriver.deriver.tracked_db", _no_db)
# Avoid executing the full reasoning pipeline; we only care about cutoff behavior.
monkeypatch.setattr(
"src.deriver.deriver.CertaintyReasoner.reason",
AsyncMock(return_value=Representation(explicit=[], deductive=[])),
)
# Create test messages with different IDs (earlier message has lower ID)
now = datetime.now(timezone.utc)
messages: list[models.Message] = []
for i in range(8):
message_id = 100 + i # 100, 101, 102, ..., 107
messages.append(
models.Message(
id=message_id,
workspace_name="test_workspace",
session_name="test_session",
peer_name="alice",
content=f"message {message_id}",
seq_in_session=i + 1,
token_count=0,
created_at=now - timedelta(minutes=7 - i),
)
)
await process_representation_tasks_batch(
observer="alice", observed="alice", messages=messages
)
# Verify that the earliest message ID was used as the cutoff
assert captured_cutoffs == [messages[0].id]