import json from pathlib import Path from typer.testing import CliRunner from reyna_cli.cli import app, build_tts_payload from reyna_cli.config import Device, Registry, get_device, load_registry from reyna_cli.utils import infer_capabilities, resolve_tool_name runner = CliRunner() def test_resolve_tool_name(): available_tools = [ {"name": "draw_text", "description": "Draw text on screen"}, {"name": "get_battery", "description": "Get battery status"}, {"name": "play_audio_base64", "description": "Play audio"}, ] assert resolve_tool_name("text", available_tools) == "draw_text" assert resolve_tool_name("battery", available_tools) == "get_battery" assert resolve_tool_name("speech", available_tools) == "play_audio_base64" assert resolve_tool_name("unknown", available_tools) is None def test_infer_capabilities(): caps = infer_capabilities([{"name": "draw_text"}, {"name": "screenshot"}]) assert caps["text"] == "draw_text" assert caps["screenshot"] == "screenshot" assert caps["speech"] is None def test_registry_mapping_yaml(tmp_path, monkeypatch): registry_path = tmp_path / "local_devices.yaml" registry_path.write_text( """ version: 1 devices: esp32_screen: display_name: ESP32 Screen host: esp32-screen.local url: http://esp32-screen.local/api/mcp fallback_url: http://192.168.68.123/api/mcp capabilities: [display, audio] """, encoding="utf-8", ) monkeypatch.setenv("REYNA_DEVICES_REGISTRY", str(registry_path)) registry = load_registry() device = get_device("screen", registry) assert device is not None assert device.id == "esp32_screen" assert device.candidate_urls() == ["http://esp32-screen.local/api/mcp", "http://192.168.68.123/api/mcp"] def test_config_get_device(): registry = Registry(devices=[Device(id="iphone_mcp", url="http://localhost:8080/api/mcp"), Device(id="personal_laptop_screen", url="http://localhost:8081/api/mcp"), Device(id="local_desktop_screen", url="http://127.0.0.1:8080/api/mcp")]) iphone = get_device("iphone", registry) laptop = get_device("laptop", registry) computer = get_device("computer", registry) assert iphone is not None assert laptop is not None assert computer is not None assert iphone.url == "http://localhost:8080/api/mcp" assert laptop.url == "http://localhost:8081/api/mcp" assert computer.url == "http://127.0.0.1:8080/api/mcp" assert get_device("nonexistent", registry) is None def test_cli_doctor(): result = runner.invoke(app, ["doctor", "--json"]) assert result.exit_code == 0 assert json.loads(result.stdout)["ok"] is True def test_cli_devices_list(monkeypatch, tmp_path): registry_path = tmp_path / "local_devices.yaml" registry_path.write_text("devices: {}\n", encoding="utf-8") monkeypatch.setenv("REYNA_DEVICES_REGISTRY", str(registry_path)) result = runner.invoke(app, ["devices", "list", "--json"]) assert result.exit_code == 0 payload = json.loads(result.stdout) assert payload["ok"] is True assert payload["devices"] == [] def test_cli_json_args_invalid_fails(): result = runner.invoke(app, ["devices", "call", "nonexistent", "tool", "--args", "{invalid}", "--json"]) assert result.exit_code != 0 assert json.loads(result.stdout)["ok"] is False def test_screen_has_subcommands(): result = runner.invoke(app, ["screen", "--help"]) assert result.exit_code == 0 assert "text" in result.stdout assert "speak" in result.stdout def test_build_tts_payload_base64_encodes_wav(monkeypatch, tmp_path): def fake_synthesize(message, wav_path, command_template=None, voice=None, sample_rate=16000): wav_path.write_bytes(b"RIFFfake-wav") return wav_path monkeypatch.setattr("reyna_cli.cli.synthesize_wav", fake_synthesize) payload = build_tts_payload("hello", tmp_path / "speech.wav", 42, None, None, 16000) assert payload == {"wav_base64": "UklGRmZha2Utd2F2", "volume": 42} def test_screen_speak_generates_wav_base64_payload(monkeypatch, tmp_path): registry_path = tmp_path / "local_devices.yaml" registry_path.write_text( """ devices: esp32_screen: display_name: ESP32 Screen url: http://screen.local/api/mcp """, encoding="utf-8", ) monkeypatch.setenv("REYNA_DEVICES_REGISTRY", str(registry_path)) def fake_get_tools(device, live_only=False, cache_only=False, refresh=False): return {"ok": True, "tools": [{"name": "play_audio_base64"}]} calls = [] def fake_call_device_tool(device, tool_name, arguments): calls.append((device.id, tool_name, arguments)) return {"ok": True, "result": "played"} def fake_synthesize(message, wav_path, command_template=None, voice=None, sample_rate=16000): assert message == "hello screen" wav_path.write_bytes(b"RIFFfake-wav") return wav_path monkeypatch.setattr("reyna_cli.cli.get_tools", fake_get_tools) monkeypatch.setattr("reyna_cli.cli.call_device_tool", fake_call_device_tool) monkeypatch.setattr("reyna_cli.cli.synthesize_wav", fake_synthesize) result = runner.invoke(app, ["screen", "speak", "hello screen", "--volume", "42", "--out", str(tmp_path / "speech.wav"), "--json"]) assert result.exit_code == 0 assert calls == [("esp32_screen", "play_audio_base64", {"wav_base64": "UklGRmZha2Utd2F2", "volume": 42})] def test_devices_screen_has_subcommands(): result = runner.invoke(app, ["devices", "screen", "--help"]) assert result.exit_code == 0 assert "text" in result.stdout def test_laptop_has_subcommands(): result = runner.invoke(app, ["laptop", "--help"]) assert result.exit_code == 0 assert "text" in result.stdout assert "speak" in result.stdout assert "screenshot" in result.stdout assert "sensors" in result.stdout def test_devices_laptop_has_subcommands(): result = runner.invoke(app, ["devices", "laptop", "--help"]) assert result.exit_code == 0 assert "text" in result.stdout assert "battery" in result.stdout def test_devices_arm_has_subcommands(): result = runner.invoke(app, ["devices", "arm", "--help"]) assert result.exit_code == 0 assert "state" in result.stdout assert "wave" in result.stdout assert "home" in result.stdout def test_immich_has_subcommands(): result = runner.invoke(app, ["immich", "--help"]) assert result.exit_code == 0 assert "stats" in result.stdout assert "albums" in result.stdout assert "search" in result.stdout def test_mongo_has_subcommands(): result = runner.invoke(app, ["mongo", "--help"]) assert result.exit_code == 0 assert "dbs" in result.stdout assert "collections" in result.stdout assert "find" in result.stdout def test_deco_has_subcommands(): result = runner.invoke(app, ["deco", "--help"]) assert result.exit_code == 0 assert "overview" in result.stdout assert "clients" in result.stdout assert "firmware" in result.stdout assert "config" in result.stdout def test_deco_config_is_direct(monkeypatch, tmp_path): empty_env = tmp_path / ".env" empty_env.write_text("", encoding="utf-8") monkeypatch.setenv("HERMES_ENV_PATH", str(empty_env)) monkeypatch.setenv("DECO_HOST", "192.168.68.1") monkeypatch.delenv("DECO_PASSWORD", raising=False) result = runner.invoke(app, ["deco", "config", "--json"]) assert result.exit_code == 0 payload = json.loads(result.stdout) assert payload["ok"] is True assert payload["source"] == "direct" assert payload["result"]["host"] == "192.168.68.1" assert payload["result"]["passwordConfigured"] is False def test_macmini_has_subcommands(): result = runner.invoke(app, ["macmini", "--help"]) assert result.exit_code == 0 assert "calendar" in result.stdout assert "contacts" in result.stdout assert "notes" in result.stdout assert "reminders" in result.stdout assert "deco" in result.stdout def test_macmini_calendar_has_subcommands(): result = runner.invoke(app, ["macmini", "calendar", "--help"]) assert result.exit_code == 0 assert "calendars" in result.stdout assert "events" in result.stdout assert "create" in result.stdout def test_macmini_deco_has_subcommands(): result = runner.invoke(app, ["macmini", "deco", "--help"]) assert result.exit_code == 0 assert "overview" in result.stdout assert "clients" in result.stdout assert "firmware" in result.stdout