"""Behaviour tests for the ticket-19 performance-baseline protocol script. The script (scripts/measure_perf_baseline.py) is the locked measurement protocol for the installed-build baseline: every number it reports comes from an observed run on the target machine class, parsed by the pure helpers tested here. Tests run offline with fixture text only and never touch audio hardware, the GPU, or the network. """ import argparse import importlib.util import json import shutil from pathlib import Path import pytest SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "measure_perf_baseline.py" SPEC = importlib.util.spec_from_file_location("measure_perf_baseline", SCRIPT) assert SPEC is not None and SPEC.loader is not None PROTOCOL = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(PROTOCOL) def test_parse_vmrss_kb_reads_kilobytes() -> None: """VmRSS lines parse to kilobytes; anything else yields None.""" status = "Name:\tvoculinux\nVmRSS:\t 412336 kB\nVmSwap:\t0 kB\n" assert PROTOCOL.parse_vmrss_kb(status) == 412336 assert PROTOCOL.parse_vmrss_kb("Name:\tvoculinux\n") is None def test_describe_reports_count_mean_min_max() -> None: """Spread statistics come from the samples, never fabricated.""" stats = PROTOCOL.describe([1.0, 2.0, 3.0]) assert stats["n"] == 3 assert stats["mean"] == pytest.approx(2.0) assert stats["min"] == pytest.approx(1.0) assert stats["max"] == pytest.approx(3.0) def test_describe_empty_is_explicit() -> None: """No samples means n == 0, never a zero mean pretending to be data.""" stats = PROTOCOL.describe([]) assert stats["n"] == 0 assert stats["mean"] is None WAKE_LOG = """\ 2026-11-05 22:44:01,100 - vocalinux.ui.action_handler - DEBUG - Handling action: toggle 2026-11-05 22:44:01,350 - vocalinux.audio.capture - DEBUG - Opened capture stream: 1 channel(s) at 16000Hz 2026-11-05 22:44:05,100 - vocalinux.ui.action_handler - DEBUG - Handling action: toggle 2026-11-05 22:44:05,500 - vocalinux.audio.capture - DEBUG - Opened capture stream: 1 channel(s) at 16000Hz """ def test_parse_wake_latencies_pairs_toggle_to_capture() -> None: """Each toggle pairs with the next capture-stream line, in seconds.""" assert PROTOCOL.parse_wake_latencies(WAKE_LOG) == pytest.approx([0.25, 0.4]) def test_parse_wake_latencies_ignores_unpaired_toggle() -> None: """A toggle with no later capture start is dropped, not zero-filled.""" log = ( WAKE_LOG + "2026-11-05 22:44:09,100 - vocalinux.ui.action_handler - DEBUG - Handling action: start\n" ) assert PROTOCOL.parse_wake_latencies(log) == pytest.approx([0.25, 0.4]) def test_realtime_factor_is_audio_over_wall() -> None: """RTF below one means faster than real time; zero audio is refused.""" assert PROTOCOL.realtime_factor(30.0, 15.0) == pytest.approx(0.5) with pytest.raises(ValueError): PROTOCOL.realtime_factor(0.0, 1.0) def test_parse_nvidia_smi_csv_reads_first_gpu() -> None: """The locked nvidia-smi query shape parses to total/free MiB.""" out = ( "name, driver_version, memory.total [MiB], memory.free [MiB]\n" "NVIDIA GeForce RTX 2050, 595.112.07, 4096 MiB, 3711 MiB\n" ) assert PROTOCOL.parse_nvidia_smi_memory(out) == (4096, 3711) assert PROTOCOL.parse_nvidia_smi_memory("no gpu here\n") is None def test_recommend_thresholds_come_from_footprints() -> None: """Ladder thresholds derive from measured footprints plus headroom.""" footprints = {"q8_0": 1000.0, "q5_0": 700.0, "small": 500.0} advice = PROTOCOL.recommend_thresholds(footprints, headroom_mib=512.0) assert advice["prefer_q8_0_above_mib"] == pytest.approx(1512.0) assert advice["prefer_q5_0_above_mib"] == pytest.approx(1212.0) assert advice["floor_small_below_mib"] == pytest.approx(1212.0) def test_render_report_names_protocol_and_host() -> None: """The evidence report carries protocol version and host facts.""" report = PROTOCOL.render_markdown_report( {"os": "Void", "gpu": "NVIDIA GeForce RTX 2050"}, {"idle": {}} ) assert PROTOCOL.PROTOCOL_VERSION in report assert "Void" in report assert "NVIDIA GeForce RTX 2050" in report def test_sample_generator_is_deterministic(tmp_path) -> None: """The fixed sample regenerates byte-identical from its seed.""" first = tmp_path / "a.wav" second = tmp_path / "b.wav" PROTOCOL.make_sample_wav(first, seed=19) PROTOCOL.make_sample_wav(second, seed=19) assert first.read_bytes() == second.read_bytes() def test_sample_shape_is_thirty_seconds_mono_16k(tmp_path) -> None: """The sample is 30 s, mono, 16 kHz, 16-bit for whisper.cpp.""" import wave target = tmp_path / "sample.wav" PROTOCOL.make_sample_wav(target, seed=19) with wave.open(str(target), "rb") as handle: assert handle.getnchannels() == 1 assert handle.getsampwidth() == 2 assert handle.getframerate() == 16000 assert handle.getnframes() / 16000 == pytest.approx(30.0) DBUS_TOGGLE_LOG = """\ 2026-11-05 23:01:11,400 - vocalinux.ui.tray_indicator - DEBUG - Toggle requested in state IDLE 2026-11-05 23:01:11,546 - vocalinux.audio.capture - DEBUG - Opened capture stream: 2 channel(s) at 44100Hz 2026-11-05 23:01:16,400 - vocalinux.ui.tray_indicator - DEBUG - Toggle requested in state LISTENING 2026-11-05 23:01:22,100 - vocalinux.ui.tray_indicator - DEBUG - Toggle requested in state IDLE 2026-11-05 23:01:22,749 - vocalinux.audio.capture - DEBUG - Opened capture stream: 2 channel(s) at 44100Hz """ def test_parse_wake_latencies_pairs_dbus_toggle_starts_only() -> None: """Only IDLE toggles open capture; a stop toggle never borrows the next run.""" assert PROTOCOL.parse_wake_latencies(DBUS_TOGGLE_LOG) == pytest.approx([0.146, 0.649]) #: Session variables the injector's niri guard reads, plus Wayland; cleared so #: the host's own session (this machine runs niri) cannot leak into a test. _SESSION_ENV = ( "NIRI_SOCKET", "XDG_CURRENT_DESKTOP", "XDG_SESSION_DESKTOP", "DESKTOP_SESSION", "WAYLAND_DISPLAY", ) class _FakeInjector: """Stands in for TextInjector: a Wayland session that selected wtype.""" wayland_mode = True wayland_tool = "wtype" def _run_inject_probe( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], session_env: dict[str, str], portal_answers: bool, ) -> tuple[dict, list[bool]]: """Run inject-probe under a fake session; return its report and portal probe calls.""" from vocalinux.text_injection import remote_desktop_portal, text_injector probes: list[bool] = [] class _SpyPortal: def probe(self) -> bool: probes.append(True) return portal_answers for name in _SESSION_ENV: monkeypatch.delenv(name, raising=False) for name, value in session_env.items(): monkeypatch.setenv(name, value) monkeypatch.setattr(text_injector, "TextInjector", _FakeInjector) monkeypatch.setattr(remote_desktop_portal, "RemoteDesktopPortal", _SpyPortal) monkeypatch.setattr(shutil, "which", lambda _name: None) assert PROTOCOL.cmd_inject_probe(argparse.Namespace()) == 0 return json.loads(capsys.readouterr().out), probes @pytest.mark.parametrize( "session_env", [ {"XDG_CURRENT_DESKTOP": "niri"}, {"NIRI_SOCKET": "/run/user/1000/niri.wayland-1.1.sock"}, ], ) def test_inject_probe_never_probes_portal_on_niri( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], session_env: dict[str, str], ) -> None: """The injector never offers the portal on niri, so the probe must not claim it. Both niri signals are covered: the probe reuses the injector's guard, so a session the injector treats as niri is reported the same way here. """ report, probes = _run_inject_probe( monkeypatch, capsys, session_env=session_env, portal_answers=True ) assert probes == [] assert report["portal_available"] is False assert report["portal_probe_seconds"] is None assert report["portal_skip_reason"] == "niri" def test_inject_probe_reports_portal_answer_off_niri( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ) -> None: """Off niri the portal is probed and its answer is reported unchanged.""" report, probes = _run_inject_probe( monkeypatch, capsys, session_env={"XDG_CURRENT_DESKTOP": "GNOME"}, portal_answers=True, ) assert probes == [True] assert report["portal_available"] is True assert report["portal_skip_reason"] is None