"""Integration: the librarian -> bot RESULT delivery contract.
A search that 'vanishes' (watchdog fires "zeżarło") means the result never
reached the bot's inbound queue. These tests pin down the contract so we can
tell a CODE break (wrong shape / uuid / auth handling) from a TRANSPORT break
(the librarian can't reach the bot at all - wrong address/port). They prove the
bot side is correct end to end, which isolates a systematic vanish to transport.
The result the librarian sends is exactly:
{uuid: {DOI: {"Title": [
...], "type": }}}
(see conjurer_librarian.answer_query -> final_result, POSTed to /conjurer).
"""
import threading
import time
import pytest
import communication_subroutine as cs
from durable_queue import DiskQueue
def _drain(queue):
while not queue.empty():
queue.get()
@pytest.fixture
def comm_threads(tmp_path, monkeypatch):
# Point the durable spool at a temp dir so /conjurer's dedup/persist can't
# leak into (or be poisoned by) the real result_inbox/ between runs.
monkeypatch.setattr(cs, "_inbox", DiskQueue(str(tmp_path / "inbox")))
monkeypatch.setattr(cs, "_delivered", DiskQueue(str(tmp_path / "delivered")))
cs.awaiting_q.clear()
_drain(cs.incoming_q)
_drain(cs.OUT_COMM_Q)
_drain(cs.IN_COMM_Q)
cs.API_KEY = None
stop = threading.Event()
workers = [
threading.Thread(target=cs.scan_queue, kwargs={"stop_event": stop}, daemon=True),
threading.Thread(target=cs.scan_incoming, kwargs={"stop_event": stop}, daemon=True),
]
for worker in workers:
worker.start()
yield
stop.set()
for worker in workers:
worker.join(timeout=3)
def _dispatch(uuid, query="kwas foliowy"):
"""Mimic the bot dispatching a search: a QueryControl enters the comm queue
and scan_queue moves it into awaiting_q."""
qc = cs.QueryControl("siara", uuid, query, None)
cs.OUT_COMM_Q.put(qc)
deadline = time.time() + 2
while time.time() < deadline:
if any(getattr(r, "uuid", None) == uuid for r in list(cs.awaiting_q)):
return qc
time.sleep(0.01)
raise AssertionError("scan_queue never moved the query into awaiting_q")
# The exact result the librarian emits for one found DOI.
def _result_payload(uuid):
return {uuid: {"10.1000/xyz": {"Title": ["A Real Paper"], "type": "journal-article"}}}
def test_librarian_result_reaches_bot_when_transport_is_fine(comm_threads):
_dispatch("uuid-ok")
client = cs.app.test_client()
resp = client.post("/conjurer", json=_result_payload("uuid-ok"))
assert resp.status_code == 200
got = cs.IN_COMM_Q.get(timeout=3)
assert got.uuid == "uuid-ok"
assert got.stop is True
# Exactly the shape check_data_q renders: entries[DOI]["Title"][0] / ["type"].
assert got.entries == {"10.1000/xyz": {"Title": ["A Real Paper"], "type": "journal-article"}}
def test_empty_result_is_still_delivered_not_vanished(comm_threads):
# A search that found nothing sends {uuid: {}} - it must STILL be delivered
# (renders "niestety nie ma nic"), never look like a lost result.
_dispatch("uuid-empty")
client = cs.app.test_client()
resp = client.post("/conjurer", json={"uuid-empty": {}})
assert resp.status_code == 200
got = cs.IN_COMM_Q.get(timeout=3)
assert got.uuid == "uuid-empty"
assert got.entries == {}
def test_wrong_api_key_rejects_result_so_it_vanishes(comm_threads):
# (b) reproduction: if the librarian's CONJURER_API_KEY differs from the
# bot's, /conjurer returns 401 and the result is never queued - the search
# silently vanishes exactly as reported.
cs.API_KEY = "bot-secret"
_dispatch("uuid-auth")
client = cs.app.test_client()
resp = client.post(
"/conjurer",
json=_result_payload("uuid-auth"),
headers={"X-Conjurer-Api-Key": "librarian-DIFFERENT-key"},
)
assert resp.status_code == 401
time.sleep(0.4)
assert cs.IN_COMM_Q.empty() # nothing delivered
def test_uuid_mismatch_orphans_result_away_from_the_querent(comm_threads):
# (b) reproduction: if the uuid the librarian echoes back doesn't byte-match
# what the bot stored, scan_incoming can't match it -> it goes to the orphan
# path (posted to the fallback channel, NOT the querent) and the querent's
# pending entry is never cleared, so the watchdog still flags it lost.
_dispatch("uuid-stored")
client = cs.app.test_client()
client.post("/conjurer", json=_result_payload("uuid-DIFFERENT"))
got = cs.IN_COMM_Q.get(timeout=3)
assert got.author == "Orphaned"
assert got.uuid == "uuid-DIFFERENT"
# The original querent's record is untouched (still awaiting) - it "vanished".
assert any(getattr(r, "uuid", None) == "uuid-stored" for r in list(cs.awaiting_q))