You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
113 lines
3.8 KiB
Python
113 lines
3.8 KiB
Python
"""Unit tests for review queue."""
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from libs.review.queue import create_review_item, list_review_items, resolve_review_item
|
|
|
|
|
|
def _make_mock_session() -> AsyncMock:
|
|
session = AsyncMock()
|
|
session.add = MagicMock()
|
|
session.flush = AsyncMock()
|
|
return session
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestCreateReviewItem:
|
|
@pytest.mark.asyncio
|
|
async def test_create_new_review_item(self) -> None:
|
|
"""Creating a review item for a new entity adds it to session."""
|
|
session = _make_mock_session()
|
|
|
|
# No existing open item
|
|
mock_result = MagicMock()
|
|
mock_result.scalar_one_or_none.return_value = None
|
|
session.execute = AsyncMock(return_value=mock_result)
|
|
|
|
await create_review_item(
|
|
session=session,
|
|
entity_type="parser_event",
|
|
entity_id="EVT::test::001",
|
|
priority="P1",
|
|
reason_codes=["low_confidence"],
|
|
snapshot_refs={"parse_id": 42},
|
|
)
|
|
|
|
session.add.assert_called_once()
|
|
session.flush.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_deduplicate_open_items(self) -> None:
|
|
"""Creating a review item for an already-open entity updates instead of creating."""
|
|
# Simulate existing open item
|
|
existing_item = MagicMock()
|
|
existing_item.status = "open"
|
|
existing_item.priority = "P2"
|
|
existing_item.reason_codes = ["low_confidence"]
|
|
existing_item.suggested_overrides = None
|
|
|
|
session = _make_mock_session()
|
|
mock_result = MagicMock()
|
|
mock_result.scalar_one_or_none.return_value = existing_item
|
|
session.execute = AsyncMock(return_value=mock_result)
|
|
|
|
await create_review_item(
|
|
session=session,
|
|
entity_type="parser_event",
|
|
entity_id="EVT::test::001",
|
|
priority="P0", # escalate from P2
|
|
reason_codes=["rule_llm_conflict"],
|
|
snapshot_refs={},
|
|
)
|
|
|
|
# Should NOT call session.add (update existing instead)
|
|
session.add.assert_not_called()
|
|
# Priority should be escalated
|
|
assert existing_item.priority == "P0"
|
|
# reason_codes merged
|
|
assert "rule_llm_conflict" in existing_item.reason_codes
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_resolve_review_item(self) -> None:
|
|
"""Resolving an open item updates status, reviewer, resolution fields."""
|
|
existing_item = MagicMock()
|
|
existing_item.status = "open"
|
|
review_id = uuid.uuid4()
|
|
|
|
session = _make_mock_session()
|
|
mock_result = MagicMock()
|
|
mock_result.scalar_one_or_none.return_value = existing_item
|
|
session.execute = AsyncMock(return_value=mock_result)
|
|
|
|
await resolve_review_item(
|
|
session=session,
|
|
review_id=review_id,
|
|
reviewer_id="analyst_01",
|
|
resolution_type="accepted",
|
|
root_cause="data looks correct",
|
|
notes="verified against source",
|
|
)
|
|
|
|
assert existing_item.status == "resolved"
|
|
assert existing_item.reviewer_id == "analyst_01"
|
|
assert existing_item.resolution_type == "accepted"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_review_items_with_status_filter(self) -> None:
|
|
"""list_review_items executes query with status filter and returns results."""
|
|
session = _make_mock_session()
|
|
mock_result = MagicMock()
|
|
mock_scalars = MagicMock()
|
|
mock_scalars.all.return_value = []
|
|
mock_result.scalars.return_value = mock_scalars
|
|
session.execute = AsyncMock(return_value=mock_result)
|
|
|
|
items = await list_review_items(session, status="open")
|
|
|
|
assert items == []
|
|
session.execute.assert_called_once()
|