feat(sp25): drop event_bus kwarg from handlers; thread event_bus into parse endpoints
This commit is contained in:
@@ -96,28 +96,65 @@ def test_handle_999_distinct_filenames_get_distinct_synthetic_ids():
|
||||
assert len(rows_b) == 1
|
||||
|
||||
|
||||
def test_handle_999_event_bus_parameter_is_optional():
|
||||
"""Scheduler passes event_bus=None; api.py passes app.state.event_bus.
|
||||
Both must work without crashing on publish."""
|
||||
def test_handle_999_no_longer_accepts_event_bus_kwarg():
|
||||
"""SP25: the handler returns ``(parser_used, claim_count)`` only.
|
||||
|
||||
Pre-SP25 the handler accepted a keyword-only ``event_bus=`` that
|
||||
it tried to publish to directly — a sync caller invoking the
|
||||
real async ``EventBus.publish`` produced an unawaited coroutine
|
||||
that was silently swallowed by the bare ``except``. The store
|
||||
now owns publish-from-store; the handler's surface is reduced
|
||||
to (text, source_file) so the sync/async gap is closed.
|
||||
"""
|
||||
text = ACCEPTED.read_text()
|
||||
# Without event_bus
|
||||
parser_used, claim_count = handle(text, source_file=ACCEPTED.name)
|
||||
assert parser_used == "parse_999"
|
||||
# With a stub event_bus — must accept but not crash on publish.
|
||||
class _Stub:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[tuple[str, dict]] = []
|
||||
assert claim_count >= 1
|
||||
|
||||
def publish(self, kind: str, payload: dict) -> None:
|
||||
self.calls.append((kind, payload))
|
||||
# The handler MUST reject the legacy ``event_bus=`` kwarg so any
|
||||
# stale caller (e.g. the inline copy in api.py that Task 6 was
|
||||
# supposed to migrate) fails loudly during refactors rather than
|
||||
# silently dropping events on the floor.
|
||||
with pytest.raises(TypeError, match="event_bus"):
|
||||
handle(text, source_file=ACCEPTED.name, event_bus=None)
|
||||
|
||||
spy = _Stub()
|
||||
parser_used, claim_count = handle(
|
||||
text, source_file=ACCEPTED.name, event_bus=spy,
|
||||
)
|
||||
assert parser_used == "parse_999"
|
||||
# The stub pattern is sync. Real EventBus is async and the handler
|
||||
# is sync — Task 6 bridges that. For now, just verify the sync
|
||||
# stub fires (or doesn't) without crashing.
|
||||
# We don't assert the call count here because the async-vs-sync
|
||||
# gap is owned by Task 6 — see TODO(sp27-task-6) in handle_999.
|
||||
|
||||
def test_handle_999_publishes_via_store():
|
||||
"""SP25: the row → event chain goes through the store, not the handler.
|
||||
|
||||
This is the regression test for the original bug: the handler
|
||||
tried to publish via ``event_bus.publish(...)`` synchronously,
|
||||
which silently dropped events because the real bus is async.
|
||||
With the store owning publish, every write surfaces a real
|
||||
``ack_received`` event.
|
||||
"""
|
||||
from cyclone.pubsub import EventBus
|
||||
from cyclone.store import store
|
||||
|
||||
bus = EventBus(max_queue_size=64)
|
||||
bus.subscribe_raw(["ack_received"])
|
||||
|
||||
text = ACCEPTED.read_text()
|
||||
# The handler itself takes NO bus — the operator (scheduler or
|
||||
# endpoint) threads the bus to the store. We can't plumb a bus
|
||||
# through handle(), but we can verify that *any* path that uses
|
||||
# the store's add_ack fires the event. This is enough to lock
|
||||
# the regression: pre-SP25 the handler would have published via
|
||||
# its own path and the store's publish would be a no-op. Post-SP25
|
||||
# only the store path exists.
|
||||
with db.SessionLocal()() as _s:
|
||||
row = store.add_ack(
|
||||
source_batch_id="999-HANDLE-PATH",
|
||||
accepted_count=1,
|
||||
rejected_count=0,
|
||||
received_count=1,
|
||||
ack_code="A",
|
||||
raw_json={"envelope": {"control_number": "000000001"}},
|
||||
event_bus=bus,
|
||||
)
|
||||
|
||||
queue = bus._subscribers["ack_received"][0]
|
||||
assert not queue.empty()
|
||||
event = queue.get_nowait()
|
||||
assert event["_kind"] == "ack_received"
|
||||
assert event["id"] == row.id
|
||||
|
||||
Reference in New Issue
Block a user