From f19c775a3a6ee8e97068792986170b5687d44b36 Mon Sep 17 00:00:00 2001 From: Steve Pinkham Date: Sun, 12 Jul 2026 12:35:06 -0400 Subject: [PATCH] fix: emit arq-session-finished on IRS success again (lost April 2024) The IRS success branch set ENDED and returned without the session-finished event, while abort, failure and the ISS success path all send one. A successful inbound raw transfer was invisible to every websocket/REST consumer, and the received payload was dropped with it (handle_raw returns the data into a call chain that discards it). c2388a65 emitted exactly this event, data= included; it vanished by ef18f4cc without that commit's IRS diff touching the lines, so this looks like a merge casualty rather than a decision. Restore the event and add a regression test: one finished event with success=True, payload round-trips through the base64 data field. The original also pushed session statistics gated on enable_stats, but that key is no longer in the config schema (config.py STATION), so the strict lookup raises KeyError; the two existing stats sites in the failure and abort paths have the same latent problem. Restoring just the event here; the stats question is worth its own look. No setARQ(False) here: dispatch() already clears busy on this path. Co-Authored-By: Claude Fable 5 --- freedata_server/arq_session_irs.py | 11 ++++++++ tests/test_arq_session.py | 41 ++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/freedata_server/arq_session_irs.py b/freedata_server/arq_session_irs.py index 477cee35..85abfff4 100644 --- a/freedata_server/arq_session_irs.py +++ b/freedata_server/arq_session_irs.py @@ -373,6 +373,17 @@ class ARQSessionIRS(arq_session.ARQSession): self.session_ended = time.time() self.set_state(IRS_State.ENDED) + session_stats = self.calculate_session_statistics(self.received_bytes, self.total_length) + self.ctx.event_manager.send_arq_session_finished( + False, + self.id, + self.dxcall, + True, + self.state.name, + data=self.received_data, + statistics=session_stats, + ) + return self.received_data, self.type_byte else: ack = self.frame_factory.build_arq_burst_ack( diff --git a/tests/test_arq_session.py b/tests/test_arq_session.py index a8376c5b..04e4366c 100644 --- a/tests/test_arq_session.py +++ b/tests/test_arq_session.py @@ -154,6 +154,47 @@ class TestARQSession(unittest.TestCase): # self.ctx_IRS.shutdown() # self.ctx_ISS.shutdown() + def testARQSessionIRSSuccessEmitsFinishedEvent(self): + # Gap 3 regression test: a completed IRS session must emit exactly + # one "arq-transfer-inbound" event with success=True, and the + # payload must round-trip through the event's base64 data field + # (previously the success branch in arq_session_irs.py never called + # send_arq_session_finished at all, so no such event was ever + # emitted -- only the abort/failure paths did). + self.loss_probability = 0 + payload = np.random.bytes(200) + + self.establishChannels() + params = { + "dxcall": "AA1AAA-1", + "data": base64.b64encode(payload), + # "raw" (not "raw_lzma"): the event's data field carries the + # bytes as received over the air, before ARQDataTypeHandler's + # separate decompression step -- "raw" keeps this an honest + # exact-match round-trip check instead of asserting equality + # against still-compressed bytes. + "type": "raw", + } + cmd = ARQRawCommand(self.ctx_ISS, params) + cmd.run() + self.waitForSession(self.ctx_ISS.TESTMODE_EVENTS, True) + self.channels_running = False + + finished_events = [] + while not self.ctx_IRS.TESTMODE_EVENTS.empty(): + ev = self.ctx_IRS.TESTMODE_EVENTS.get() + if "arq-transfer-inbound" in ev: + finished_events.append(ev["arq-transfer-inbound"]) + + successes = [e for e in finished_events if e.get("success") is True] + self.assertEqual( + len(successes), + 1, + f"expected exactly one successful arq-transfer-inbound event, got {finished_events}", + ) + received = base64.b64decode(successes[0]["data"]) + self.assertEqual(received, payload) + def DisabledtestARQSessionAbortTransmissionISS(self): # set Packet Error Rate (PER) / frame loss probability self.loss_probability = 0