186 lines
6.8 KiB
Python
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]
|