tests: pin the librarian->bot result delivery contract
CI / compile (pull_request) Successful in 19s
CI / unit (pull_request) Successful in 23s
CI / integration (pull_request) Successful in 27s
build / build (push) Successful in 27s
CI / compile (push) Successful in 9s
CI / unit (push) Successful in 20s
CI / integration (push) Successful in 26s
CI / compile (pull_request) Successful in 19s
CI / unit (pull_request) Successful in 23s
CI / integration (pull_request) Successful in 27s
build / build (push) Successful in 27s
CI / compile (push) Successful in 9s
CI / unit (push) Successful in 20s
CI / integration (push) Successful in 26s
Diagnostic coverage for the 'search vanished' report. Proves the bot side of result delivery is correct end to end (right shape reaches IN_COMM_Q; empty result still delivered; wrong api-key -> 401 vanish; uuid mismatch -> orphaned away from the querent), which isolates a SYSTEMATIC vanish to transport: the librarian being unable to reach the bot's /conjurer at all (CONJURER_MAIN_BOT). The bot Service is NodePort with no pinned nodePort while the librarian hardcodes :32442 - and being in-cluster it should use the Service DNS http://bot:5000 instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit was merged in pull request #11.
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
"""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": [<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))
|
||||
Reference in New Issue
Block a user