import asyncio from datetime import datetime, timezone import app.webhooks_chatwoot as webhook import app.persistence as persistence from app.persistence import _message_created_at def _payload(*, private=False, message_id=712, content="Resposta pública"): return { "event": "message_created", "id": message_id, "message_type": "outgoing", "private": private, "content": content, "created_at": 1_700_000_000, "conversation": {"id": 2450}, "sender": {"id": 9, "name": "Operador", "type": "user"}, } def test_chatwoot_timestamp_preserves_epoch_and_iso(): assert _message_created_at(1_700_000_000) == datetime.fromtimestamp(1_700_000_000, tz=timezone.utc) assert _message_created_at("2026-08-15T10:20:30Z") == datetime(2026, 8, 15, 10, 20, 30, tzinfo=timezone.utc) def test_outgoing_public_persists_fact_before_existing_outgoing_processing(monkeypatch): calls = [] monkeypatch.setattr(webhook, "save_factual_chatwoot_message", lambda **kw: (calls.append(("message", kw)) or ("msg-1", True))) monkeypatch.setattr(webhook, "auto_complete_task_from_outgoing_message", lambda **kw: (calls.append(("outgoing", kw)) or {"status": "no_matching_pending_task"})) monkeypatch.setattr(webhook, "mark_raw_event_processed", lambda **kw: calls.append(("processed", kw))) result = asyncio.run(webhook.process_saved_chatwoot_raw_event("00000000-0000-0000-0000-000000000001", _payload())) assert [call[0] for call in calls] == ["message", "outgoing", "processed"] assert calls[0][1]["direction"] == "outbound" assert calls[0][1]["source_event_id"] == "712" assert calls[0][1]["contact_id"] is None assert calls[0][1]["source_created_at"] == 1_700_000_000 assert result["message_id"] == "msg-1" def test_outgoing_public_does_not_enter_inbound_analysis(monkeypatch): monkeypatch.setattr(webhook, "save_factual_chatwoot_message", lambda **kw: ("msg-1", True)) monkeypatch.setattr(webhook, "auto_complete_task_from_outgoing_message", lambda **kw: {"status": "no_matching_pending_task"}) monkeypatch.setattr(webhook, "mark_raw_event_processed", lambda **kw: None) async def forbidden(*args, **kwargs): raise AssertionError("outgoing factual messages must not enter classification") monkeypatch.setattr(webhook, "analyze", forbidden) asyncio.run(webhook.process_saved_chatwoot_raw_event("00000000-0000-0000-0000-000000000001", _payload())) def test_outgoing_private_note_is_not_persisted_as_public_message(monkeypatch): monkeypatch.setattr(webhook, "save_factual_chatwoot_message", lambda **kw: (_ for _ in ()).throw(AssertionError("private note persisted"))) monkeypatch.setattr(webhook, "auto_complete_task_from_outgoing_message", lambda **kw: {"status": "ignored_private_note"}) monkeypatch.setattr(webhook, "mark_raw_event_processed", lambda **kw: None) result = asyncio.run(webhook.process_saved_chatwoot_raw_event("00000000-0000-0000-0000-000000000001", _payload(private=True))) assert result["message_id"] is None def test_string_false_private_value_is_public(): assert webhook.extract_chatwoot_event(_payload(private="false"))["is_private"] is False def test_non_message_created_outgoing_event_is_not_persisted(monkeypatch): payload = _payload() payload["event"] = "conversation_updated" monkeypatch.setattr(webhook, "save_factual_chatwoot_message", lambda **kw: (_ for _ in ()).throw(AssertionError("wrong event persisted"))) monkeypatch.setattr(webhook, "auto_complete_task_from_outgoing_message", lambda **kw: {"status": "no_matching_pending_task"}) monkeypatch.setattr(webhook, "mark_raw_event_processed", lambda **kw: None) asyncio.run(webhook.process_saved_chatwoot_raw_event("00000000-0000-0000-0000-000000000001", payload)) class _Rows: def __init__(self, row=None): self.row = row def first(self): return self.row class _PersistenceConnection: def __init__(self): self.message_id = None self.inserts = 0 def execute(self, statement, params=None): sql = str(statement) if "pg_advisory_xact_lock" in sql: return _Rows() if "SELECT id::text FROM messages" in sql: return _Rows((self.message_id,)) if self.message_id else _Rows() if "INSERT INTO messages" in sql: self.inserts += 1 self.message_id = "00000000-0000-0000-0000-000000000099" return _Rows((self.message_id,)) if "UPDATE raw_events SET message_id" in sql: return _Rows() raise AssertionError(sql) class _Begin: def __init__(self, connection): self.connection = connection def __enter__(self): return self.connection def __exit__(self, *_args): return False class _PersistenceEngine: def __init__(self): self.connection = _PersistenceConnection() def begin(self): return _Begin(self.connection) def test_factual_persistence_is_idempotent_by_source_identity(monkeypatch): fake = _PersistenceEngine() monkeypatch.setattr(persistence, "engine", fake) monkeypatch.setattr(persistence.settings, "clientflow_persist", True) kwargs = { "raw_event_id": "00000000-0000-0000-0000-000000000001", "source_event_id": "712", "conversation_id": "2450", "contact_id": None, "direction": "outbound", "raw_body": "Resposta", "source_created_at": 1_700_000_000, } first = persistence.save_factual_chatwoot_message(**kwargs) second = persistence.save_factual_chatwoot_message(**kwargs) assert first[1] is True assert second == (first[0], False) assert fake.connection.inserts == 1 class _BackfillPresence: def __init__(self, present=False): self.present = present def execute(self, statement, params=None): return _Rows((1,)) if self.present else _Rows() def rollback(self): return None def __enter__(self): return self def __exit__(self, *_args): return False class _BackfillEngine: def __init__(self, presence): self.presence = presence def connect(self): return self.presence def test_outbound_backfill_dry_run_apply_and_repeat(monkeypatch): import scripts.backfill_chatwoot_outbound_messages as backfill row = { "id": "00000000-0000-0000-0000-000000000001", "source_event_id": "712", "conversation_id": "2450", "contact_id": None, "payload": _payload(), "created_at": datetime(2026, 8, 15, tzinfo=timezone.utc), } presence = _BackfillPresence(False) monkeypatch.setattr(backfill, "_candidate_rows", lambda: [row]) monkeypatch.setattr(backfill, "engine", _BackfillEngine(presence)) writes = [] def save(**kwargs): writes.append(kwargs) presence.present = True return "msg-1", True monkeypatch.setattr(backfill, "save_factual_chatwoot_message", save) assert backfill.backfill(apply=False) == { "candidates": 1, "inserted": 0, "already_present": 0, "skipped": 0, "errors": 0, } assert writes == [] assert backfill.backfill(apply=True)["inserted"] == 1 repeated = backfill.backfill(apply=True) assert repeated["inserted"] == 0 assert repeated["already_present"] == 1 assert len(writes) == 1