From 085fcdf1a112436a306af86d198f82de05a95e9d Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Wed, 5 Aug 2026 00:14:05 -0400 Subject: [PATCH] Fix test suite: pad expectations, container pytest config, asyncio markers Three unrelated failures from `make test` on the node: test_metadata_watcher: two tests asserted the old zero-pad tgid_change contract. Both were off by exactly call_tail_pad_seconds, i.e. the code was doing what f1de157 intended and the tests encoded the behaviour we deliberately changed. Updated to expect the pad. test_pulse: four async tests errored with "async def functions are not natively supported". Root cause is not the tests - pytest.ini sets asyncio_mode = auto but the Dockerfile only copies app/ and tests/, so the container had no pytest config at all and fell back to strict mode. That is also why these passed in a local venv and failed on the node. Copy pytest.ini into the image, and add explicit @pytest.mark.asyncio so the tests hold up regardless of how pytest is configured. Co-Authored-By: Claude Opus 5 --- drb-edge-node/Dockerfile | 3 +++ drb-edge-node/tests/test_metadata_watcher.py | 11 +++++++---- drb-edge-node/tests/test_pulse.py | 6 ++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/drb-edge-node/Dockerfile b/drb-edge-node/Dockerfile index 532dd72..2f31af5 100644 --- a/drb-edge-node/Dockerfile +++ b/drb-edge-node/Dockerfile @@ -15,5 +15,8 @@ RUN pip install uv && uv pip install --system --no-cache-dir -r requirements.txt COPY app/ ./app/ COPY tests/ ./tests/ +# Without this the container runs pytest with asyncio_mode defaulting to strict, +# so unmarked async tests error out even though they pass locally. +COPY pytest.ini . CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80", "--reload"] diff --git a/drb-edge-node/tests/test_metadata_watcher.py b/drb-edge-node/tests/test_metadata_watcher.py index 4b1ce13..65e8b86 100644 --- a/drb-edge-node/tests/test_metadata_watcher.py +++ b/drb-edge-node/tests/test_metadata_watcher.py @@ -452,9 +452,11 @@ async def test_different_tgid_grant_splits_recording(watcher, clock): assert ended["call_id"] == first_id assert ended["tgid"] == 1111 assert ended["end_reason"] == "tgid_change" - # The outgoing segment ends exactly where the new one begins — no tail pad, - # or it would swallow the first moments of the new talkgroup. - assert ended["ended_at_epoch"] == split_time + # The outgoing segment is padded PAST where the new one begins. The buffered + # audio lags control-channel timestamps by ~1.5s, so ending exactly at the + # split cut the outgoing call's last words. The overlap is correct — the + # audio stream really does hold one call's tail then the next call's start. + assert ended["ended_at_epoch"] == split_time + settings.call_tail_pad_seconds assert watcher.on_call_start.call_args[0][0]["started_at_epoch"] == split_time @@ -482,7 +484,8 @@ async def test_multiple_call_log_entries_in_one_poll(watcher, clock): assert ended["tgid"] == 1111 assert ended["transmissions"] == 2 assert ended["started_at_epoch"] == t0 - assert ended["ended_at_epoch"] == t0 + 0.9 + # Split close is padded past the new grant — see the tgid_change test above. + assert ended["ended_at_epoch"] == t0 + 0.9 + settings.call_tail_pad_seconds @pytest.mark.asyncio diff --git a/drb-edge-node/tests/test_pulse.py b/drb-edge-node/tests/test_pulse.py index aef07fe..7d035aa 100644 --- a/drb-edge-node/tests/test_pulse.py +++ b/drb-edge-node/tests/test_pulse.py @@ -17,6 +17,8 @@ is proven at the layer that matters. import subprocess from unittest.mock import Mock +import pytest + from app.internal import pulse @@ -53,6 +55,7 @@ def test_is_ready_false_when_daemon_does_not_respond(monkeypatch): assert pulse.is_ready() is False +@pytest.mark.asyncio async def test_wait_until_ready_short_circuits_when_already_live(monkeypatch): calls = Mock(return_value=True) monkeypatch.setattr(pulse, "_daemon_responds", calls) @@ -60,6 +63,7 @@ async def test_wait_until_ready_short_circuits_when_already_live(monkeypatch): assert calls.call_count == 1 +@pytest.mark.asyncio async def test_wait_until_ready_polls_until_daemon_comes_up(monkeypatch): monkeypatch.setattr(pulse, "POLL_INTERVAL", 0.01) responses = iter([False, False, True]) @@ -67,12 +71,14 @@ async def test_wait_until_ready_polls_until_daemon_comes_up(monkeypatch): assert await pulse.wait_until_ready(timeout=5) is True +@pytest.mark.asyncio async def test_wait_until_ready_times_out_when_daemon_never_responds(monkeypatch): monkeypatch.setattr(pulse, "POLL_INTERVAL", 0.01) monkeypatch.setattr(pulse, "_daemon_responds", lambda: False) assert await pulse.wait_until_ready(timeout=0.05) is False +@pytest.mark.asyncio async def test_wait_until_ready_uses_settings_default_when_timeout_omitted(monkeypatch): monkeypatch.setattr(pulse.settings, "pulse_wait_timeout", 0.05) monkeypatch.setattr(pulse, "POLL_INTERVAL", 0.01)