"""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": <str>}}} (see conjurer_librarian.answer_query -> final_result, POSTed to /conjurer). """ import threading import time import pytest import communication_subroutine as cs def _drain(queue): while not queue.empty(): queue.get() @pytest.fixture def comm_threads(): 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))