""" Tests for EntityResolver edge cases. """ import uuid from datetime import datetime, timezone import asyncpg import pytest from hindsight_api.engine.entity_resolver import EntityResolver from hindsight_api.pg0 import resolve_database_url # --------------------------------------------------------------------------- # Unit tests for discard_pending_stats() — no database required # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_discard_pending_stats_clears_both_dicts(): """discard_pending_stats() must remove entries for the current task from both _pending_stats and _pending_cooccurrences.""" resolver = EntityResolver(pool=None) # type: ignore[arg-type] key = resolver._task_key() resolver._pending_stats[key] = [object()] # type: ignore[list-item] resolver._pending_cooccurrences[key] = [object()] # type: ignore[list-item] resolver.discard_pending_stats() assert key not in resolver._pending_stats assert key not in resolver._pending_cooccurrences @pytest.mark.asyncio async def test_discard_pending_stats_is_idempotent(): """Calling discard_pending_stats() when nothing is pending must not raise.""" resolver = EntityResolver(pool=None) # type: ignore[arg-type] resolver.discard_pending_stats() resolver.discard_pending_stats() # second call — still safe @pytest.mark.asyncio async def test_discard_pending_stats_does_not_affect_other_task_keys(): """discard_pending_stats() must only remove the current task's entries, leaving entries keyed under other task IDs untouched.""" resolver = EntityResolver(pool=None) # type: ignore[arg-type] other_key = -1 # A fake key that can never be a real task id resolver._pending_stats[other_key] = [object()] # type: ignore[list-item] resolver._pending_cooccurrences[other_key] = [object()] # type: ignore[list-item] resolver.discard_pending_stats() # discards current task's key only assert other_key in resolver._pending_stats, "other task's stats must be preserved" assert other_key in resolver._pending_cooccurrences, "other task's cooccurrences must be preserved" @pytest.mark.asyncio async def test_resolve_entities_batch_handles_unicode_lower_conflicts(pg0_db_url): """ Existing entities with PostgreSQL/Python lowercase mismatches should resolve to the conflicted row instead of leaving a missing entity_id. """ resolved_url = await resolve_database_url(pg0_db_url) pool = await asyncpg.create_pool(resolved_url, min_size=1, max_size=2, command_timeout=30) bank_id = f"test-entity-resolver-{uuid.uuid4().hex[:8]}" event_date = datetime(2024, 1, 15, tzinfo=timezone.utc) resolver = EntityResolver(pool=pool, entity_lookup="full") try: async with pool.acquire() as conn: existing_entity_id = await conn.fetchval( """ INSERT INTO entities (bank_id, canonical_name, first_seen, last_seen, mention_count) VALUES ($1, $2, $3, $3, 1) RETURNING id """, bank_id, "İstanbul", event_date, ) resolved_ids = await resolver.resolve_entities_batch( bank_id=bank_id, entities_data=[ { "text": "istanbul", "nearby_entities": [], "event_date": event_date, } ], context="unicode case mismatch", unit_event_date=event_date, conn=conn, ) entity_rows = await conn.fetch( """ SELECT id, canonical_name FROM entities WHERE bank_id = $1 ORDER BY canonical_name """, bank_id, ) assert resolved_ids == [existing_entity_id] assert len(entity_rows) == 1 assert entity_rows[0]["id"] == existing_entity_id assert entity_rows[0]["canonical_name"] == "İstanbul" finally: await pool.execute("DELETE FROM entities WHERE bank_id = $1", bank_id) await pool.close()