import json from pathlib import Path import httpx from typer.testing import CliRunner from reyna_cli.cli import app from reyna_cli.config import Device, Registry, get_device, resolve_device_host from reyna_cli.tactility import TactilityClient runner = CliRunner() def test_friendly_name_lookup_is_case_insensitive(): registry = Registry(devices=[Device(id="kidsos_5c5c", display_name="Grace's 2.8-inch Tactility Board")]) assert get_device("Grace", registry).id == "kidsos_5c5c" assert get_device("grace", registry).id == "kidsos_5c5c" def test_resolve_device_host_prefers_live_mdns(monkeypatch): monkeypatch.setattr("reyna_cli.config.socket.gethostbyname", lambda host: "192.168.68.141") device = Device(id="kidsos_1234", host="kidsos-1234.local", reserved_ip="192.168.68.99") assert resolve_device_host(device) == "192.168.68.141" def test_resolve_device_host_falls_back_when_mdns_is_offline(monkeypatch): def fail(_host): raise OSError("offline") monkeypatch.setattr("reyna_cli.config.socket.gethostbyname", fail) device = Device(id="kidsos_1234", host="kidsos-1234.local", reserved_ip="192.168.68.99") assert resolve_device_host(device) == "192.168.68.99" def test_tactility_client_uses_web_api(monkeypatch, tmp_path): requests = [] def handler(request: httpx.Request): requests.append(request) if request.url.path == "/api/sysinfo": return httpx.Response(200, json={"version": "0.8.0-dev"}) if request.url.path == "/api/apps": return httpx.Response(200, json={"apps": [{"id": "one.tactility.demo"}]}) if request.url.path == "/fs/list": return httpx.Response(200, json={"path": "/sdcard", "entries": []}) return httpx.Response(200, json={"ok": True}) client = TactilityClient("http://kidsos-1234.local", transport=httpx.MockTransport(handler)) assert client.sysinfo()["version"] == "0.8.0-dev" assert client.apps()["apps"][0]["id"] == "one.tactility.demo" assert client.fs_list("/sdcard")["path"] == "/sdcard" assert requests[0].url.host == "kidsos-1234.local" def test_tactility_install_and_run_requests(tmp_path): requests = [] def handler(request: httpx.Request): requests.append(request) return httpx.Response(200, json={"ok": True}) app_file = tmp_path / "demo.app" app_file.write_bytes(b"APP") client = TactilityClient("http://192.168.68.99", transport=httpx.MockTransport(handler)) client.install_app(app_file) client.run_app("one.tactility.demo") assert requests[0].method == "PUT" assert requests[0].url.path == "/api/apps/install" assert requests[1].url.path == "/api/apps/run" assert requests[1].url.params["id"] == "one.tactility.demo" def test_tactility_screen_text_clears_before_write(): requests = [] def handler(request: httpx.Request): requests.append(request) return httpx.Response(200, json={"ok": True}) client = TactilityClient("http://192.168.68.99", transport=httpx.MockTransport(handler)) result = client.screen_text("Grace", 20, 10, clear_first=True) assert result["clear_first"] is True assert [request.url.path for request in requests] == ["/api/screen/raw", "/api/screen/raw"] assert requests[0].content == bytes(20 * 10 * 2) assert len(requests[1].content) == 20 * 10 * 2 def test_tactility_cli_has_commands(monkeypatch, tmp_path): registry_path = tmp_path / "devices.yaml" registry_path.write_text( """ devices: kidsos_1234: type: esp32-s3-tactility host: kidsos-1234.local reserved_ip: 192.168.68.99 """, encoding="utf-8", ) monkeypatch.setenv("REYNA_DEVICES_REGISTRY", str(registry_path)) result = runner.invoke(app, ["tactility", "--help"]) assert result.exit_code == 0 for command in ("discover", "sysinfo", "apps", "install", "run", "report", "fs", "screen"): assert command in result.stdout def test_robot_arm_is_not_a_cli_surface(): result = runner.invoke(app, ["devices", "--help"]) assert result.exit_code == 0 assert "arm" not in result.stdout.lower()