bandsaunter/tests/conftest.py
The Dust Council 8a789e57e1 Hear the short replies, and read the other kind of callsign
Two things, both found by measuring rather than by reading the code.

The voice-activity filter inside the recogniser is off. It was costing
words: across a night of land-mobile captures it dropped 5-15% of what
the same model finds without it -- 491 against 507, 339 against 384,
263 against 310 -- because a single-word over between two
transmissions looks to a VAD exactly like the noise it exists to
remove, and on a scanner those short replies are the ones worth
having.

Turning it off has a cost, and the cost is that Whisper hands back
"You" for five seconds of hiss as confidently as it hands back a
sentence. So the whole capture is now asked once whether anything in
it rises above its own noise. Digital silence measures 0.0 dB of
contrast and hiss at any level 0.7, while the quietest real capture of
that night measures 8.9 and most measure 10-27; the bar sits at 3, an
order of magnitude clear of both. It can veto a capture but never trim
one, which is the whole difference between it and the filter it
replaces.

The second thing: callsigns like WQVF960 were being missed entirely.
The shape being matched was the amateur one -- prefix, district digit,
suffix -- and everything else the FCC licenses is written the other
way round, the letters first and then the digits. On the GMRS and
business channels that is most of what is said: nine callsigns across
five transcripts of one evening went by unrecognised, and now do not.

The shape is written as the three allocations that exist rather than
as "letters then digits", which claims KN95, WD40 and KC135. Its
letters are checked against the word list even when they arrive as a
single token, which the amateur shape does not need -- no English word
has a digit in the middle of it, but "west 120" and "word 100" fit
this one exactly.

Lookups now fall back to hamdb.org when callook has nothing. Not a
spare copy: callook holds United States amateur licences only, so
DL1ABC and VE3ABC are INVALID there and resolve perfectly well from
the other. And a GMRS callsign is not looked up at all -- every
database reachable without an account is an amateur register, so
reporting WQVF960 as "unlisted" would blame the callsign for the
absence of a source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PsWPTweCT6pwxKngvVxcg
2026-09-02 00:30:02 -07:00

59 lines
2.3 KiB
Python

"""Fixtures every test gets.
All three are about not touching the machine the tests run on, or anyone
else's.
The cache: a lookup writes to ``~/.cache`` by default, and a test run that
touches the real one leaves entries behind and reads back entries an earlier
version wrote. Redirecting it per test makes each run start from nothing.
The settings: locking a frequency out writes it into ``config.yaml``, and a
test that reached the real one would silently change what the next real scan
does. The constant is replaced in every module that holds a copy, so that
forgetting to pass a directory somewhere cannot end in someone's own settings.
The network: a licence lookup goes to a public database and returns a real
person's name and address. No test has any business doing that.
"""
import pytest
import bandsaunter.browse
import bandsaunter.callsign
import bandsaunter.cli
import bandsaunter.config
import bandsaunter.tui
@pytest.fixture(autouse=True)
def isolated_cache(tmp_path_factory, monkeypatch):
monkeypatch.setenv("XDG_CACHE_HOME",
str(tmp_path_factory.mktemp("cache")))
@pytest.fixture(autouse=True)
def no_licence_lookups(monkeypatch):
"""No test may contact the licence database.
One did, silently, and passed -- it was only visible because the
assertion it failed printed a real operator's address. A test that wants
answers stubs this itself; anything else fails loudly rather than going
to the network and being slow, flaky and rude about it.
"""
def refuse(self, url, call):
raise AssertionError(f"a test tried to look up {call} for real")
monkeypatch.setattr(bandsaunter.callsign.CallsignBook, "_request", refuse)
@pytest.fixture(autouse=True)
def isolated_settings(tmp_path_factory, monkeypatch):
where = tmp_path_factory.mktemp("config")
for module in (bandsaunter.config, bandsaunter.browse, bandsaunter.cli,
bandsaunter.tui):
if hasattr(module, "DEFAULT_CONFIG_DIR"):
monkeypatch.setattr(module, "DEFAULT_CONFIG_DIR", where)
if hasattr(module, "DEFAULT_CONFIG_PATH"):
monkeypatch.setattr(module, "DEFAULT_CONFIG_PATH",
where / "config.yaml")
monkeypatch.setenv("BANDSAUNTER_CONFIG_DIR", str(where))
return where