"""The preflight. Its one job is to never be the thing that's broken. A doctor that raises on a broken install diagnoses the wrong patient, so the tests that matter here are the ugly-input ones: no config at all, a check that throws, a dependency missing. The individual diagnoses are simple enough to read; that they *run* on a machine missing everything is the property worth pinning down. """ import sys from pathlib import Path from unittest.mock import MagicMock, patch import pytest sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from bolt_pet import config, doctor from bolt_pet.doctor import FAIL, OK, WARN def test_every_check_returns_a_verdict_on_a_bare_machine(monkeypatch): """Nothing configured, nothing installed — still a full report.""" for name in ("SERVER_URL", "API_KEY", "ELEVENLABS_API_KEY", "ELEVENLABS_VOICE_ID"): monkeypatch.setattr(config, name, "") monkeypatch.setattr(doctor, "_module", lambda _n: False) results = doctor.run() assert len(results) == len(doctor.CHECKS) assert all(c.status in (OK, WARN, FAIL) for c in results) assert all(c.name and c.detail for c in results) def test_a_check_that_raises_does_not_hide_the_others(): """One broken probe must not cost you the other eleven diagnoses.""" def explode(*_args): raise RuntimeError("boom") original = doctor.CHECKS doctor.CHECKS = (("mic", explode),) + original[:2] try: results = doctor.run() finally: doctor.CHECKS = original assert len(results) == 3 assert results[0].status == FAIL assert "boom" in results[0].detail def test_missing_server_config_is_a_failure_not_a_warning(monkeypatch): """Without it the controller exits its thread at startup — the pet looks alive and simply never answers. That is the worst failure mode there is.""" monkeypatch.setattr(config, "missing_config", lambda: ["BOLT_SERVER_URL", "DESK_API_KEY"]) check = doctor.check_config() assert check.status == FAIL assert "BOLT_SERVER_URL" in check.detail assert check.fix def test_a_configured_server_passes_without_being_contacted(monkeypatch): """The shallow run must not need the network — it's the first thing you reach for when the network is what's wrong.""" monkeypatch.setattr(config, "missing_config", lambda: []) monkeypatch.setattr(config, "SERVER_URL", "http://bolt.local:8000") assert doctor.check_config().status == OK assert doctor.check_server(deep=False).status == OK def test_every_problem_comes_with_something_to_do_about_it(monkeypatch): """"screen reading: warn" is useless on its own; "apt install tesseract-ocr" is the entire point of the tool.""" for name in ("SERVER_URL", "API_KEY"): monkeypatch.setattr(config, name, "") monkeypatch.setattr(doctor, "_module", lambda _n: False) for check in doctor.run(): if check.status == FAIL: assert check.fix, f"{check.name} says what's wrong but not what to do" def test_the_exit_code_is_nonzero_only_for_real_failures(monkeypatch, capsys): monkeypatch.setattr(doctor, "run", lambda deep=False: [ doctor.Check("a", OK, "fine"), doctor.Check("b", WARN, "degraded")]) assert doctor.main([]) == 0 monkeypatch.setattr(doctor, "run", lambda deep=False: [doctor.Check("a", FAIL, "broken")]) assert doctor.main([]) == 1 assert "Fix those first" in capsys.readouterr().out def test_deep_is_off_unless_asked(monkeypatch): seen = [] monkeypatch.setattr(doctor, "run", lambda deep=False: seen.append(deep) or []) doctor.main([]) doctor.main(["--deep"]) assert seen == [False, True] def test_a_slow_silence_timeout_is_flagged(monkeypatch): """The setting most likely to make it feel sluggish, and the least obvious — it is pure dead air before anything at all starts happening.""" monkeypatch.setattr(config, "SILENCE_END_SEC", 2.0) check = doctor.check_latency() assert check.status == WARN assert "2s" in check.detail or "2 " in check.detail monkeypatch.setattr(config, "SILENCE_END_SEC", 0.9) assert doctor.check_latency().status == OK def test_a_check_line_renders_the_fix_only_when_there_is_a_problem(): assert "→" not in doctor.Check("x", OK, "all good", fix="unused").line() assert "→ do the thing" in doctor.Check("x", WARN, "hmm", fix="do the thing").line() # ── check_tts's --deep probe ──────────────────────────────────────────────── # check_streaming_endpoint already established the pattern: shallow only # checks config (no network), --deep actually calls the server. TTS needs # its own deep probe specifically because "configured but broken" (a # rejected voice id, a dead credit balance) is otherwise indistinguishable # from "working" until speak() silently falls back to the offline voice — # which is exactly the bug report this was added to diagnose. @pytest.fixture(autouse=True) def _tts_configured(monkeypatch): monkeypatch.setattr(config, "SERVER_URL", "http://test-server:5002") monkeypatch.setattr(config, "API_KEY", "test-key") monkeypatch.setattr(config, "SESSION_ID", "pet-test") monkeypatch.setattr(config, "ELEVENLABS_VOICE_ID", "abc123voiceid") def test_tts_shallow_does_not_touch_the_network(): with patch("requests.post") as post: check = doctor.check_tts(deep=False) post.assert_not_called() assert check.status == OK assert "not probed" in check.detail def test_tts_deep_reports_success(): response = MagicMock(status_code=200, content=b"\x00\x01" * 100) with patch("requests.post", return_value=response) as post: check = doctor.check_tts(deep=True) post.assert_called_once() assert check.status == OK assert "200 bytes" in check.detail args, kwargs = post.call_args assert args[0] == "http://test-server:5002/desk/tts" assert kwargs["headers"] == {"X-Desk-Api-Key": "test-key"} assert kwargs["json"]["voice_id"] == "abc123voiceid" def test_tts_deep_surfaces_the_servers_error_message(): """A 404 with 'voice not found' is exactly the shape of the reported bug: a vb: clone owned by someone else, or a stale/mistyped id.""" response = MagicMock(status_code=404) response.json.return_value = {"error": "voice not found"} with patch("requests.post", return_value=response): check = doctor.check_tts(deep=True) assert check.status == FAIL assert "404" in check.detail assert "voice not found" in check.detail assert check.fix def test_tts_deep_handles_a_network_failure_without_raising(): with patch("requests.post", side_effect=OSError("no route to host")): check = doctor.check_tts(deep=True) assert check.status == FAIL assert "no route to host" in check.detail def test_tts_run_wires_deep_through_automatically(monkeypatch): """run()'s TypeError-fallback dance (doctor.py's CHECKS loop) must actually reach check_tts's deep parameter, not silently call it shallow.""" seen = [] def fake_check_tts(deep=False): seen.append(deep) return doctor.Check("text-to-speech", OK, "stub") monkeypatch.setattr(doctor, "CHECKS", (("tts", fake_check_tts),)) doctor.run(deep=True) assert seen == [True]