feat: expand Reyna CLI integrations
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from reyna_cli.cli import app
|
||||
from reyna_cli.desktop_service import SERVICE_NAME, unit_content
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def test_computer_has_device_and_service_subcommands():
|
||||
result = runner.invoke(app, ["computer", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "text" in result.stdout
|
||||
assert "screenshot" in result.stdout
|
||||
assert "service-install" in result.stdout
|
||||
assert "service-status" in result.stdout
|
||||
|
||||
|
||||
def test_devices_computer_has_subcommands():
|
||||
result = runner.invoke(app, ["devices", "computer", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "service-start" in result.stdout
|
||||
assert "battery" in result.stdout
|
||||
|
||||
|
||||
def test_unit_content_matches_desktop_client_gui_session():
|
||||
content = unit_content()
|
||||
assert SERVICE_NAME == "reyna-desktop-client.service"
|
||||
assert "Environment=WAYLAND_DISPLAY=wayland-0" in content
|
||||
assert "Environment=XDG_RUNTIME_DIR=/run/user/1000" in content
|
||||
assert "ExecStart=/usr/bin/python3 /home/adolforeyna/Projects/Screen/desktop_client/client.py" in content
|
||||
assert "Restart=always" in content
|
||||
@@ -0,0 +1,74 @@
|
||||
import json
|
||||
import mailbox
|
||||
from email.message import EmailMessage
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from reyna_cli.cli import app
|
||||
from reyna_cli.email_local import ThunderbirdEmailClient
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def make_profile(tmp_path):
|
||||
profile = tmp_path / "profile"
|
||||
inbox = profile / "ImapMail" / "imap.example.com" / "INBOX"
|
||||
inbox.parent.mkdir(parents=True)
|
||||
mbox = mailbox.mbox(str(inbox))
|
||||
msg = EmailMessage()
|
||||
msg["From"] = "The Home Depot Protection Plan Team <purchaseconfirmation@squaretrade.com>"
|
||||
msg["To"] = "adolfo@example.com"
|
||||
msg["Subject"] = "Thank You for Your The Home Depot Protection Plan Purchase"
|
||||
msg["Date"] = "Mon, 01 Sep 2025 04:20:20 +0000"
|
||||
msg["Message-ID"] = "<fridge@example.com>"
|
||||
msg.set_content("Thank you for purchasing your new Whirlpool Refrigerator WRS321SDHZ Protection Plan.")
|
||||
mbox.add(msg)
|
||||
mbox.flush()
|
||||
mbox.close()
|
||||
return profile, inbox
|
||||
|
||||
|
||||
def test_email_client_searches_local_thunderbird_mbox(tmp_path, monkeypatch):
|
||||
profile, _ = make_profile(tmp_path)
|
||||
monkeypatch.setenv("REYNA_THUNDERBIRD_PROFILE", str(profile))
|
||||
|
||||
result = ThunderbirdEmailClient().search("Whirlpool Refrigerator", limit=5)
|
||||
|
||||
assert result["results"]
|
||||
first = result["results"][0]
|
||||
assert first["subject"] == "Thank You for Your The Home Depot Protection Plan Purchase"
|
||||
assert "Whirlpool Refrigerator" in " ".join(first["snippets"])
|
||||
|
||||
|
||||
def test_email_cli_has_subcommands():
|
||||
result = runner.invoke(app, ["email", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "search" in result.stdout
|
||||
assert "latest" in result.stdout
|
||||
assert "find-receipts" in result.stdout
|
||||
|
||||
|
||||
def test_email_cli_search_json(tmp_path, monkeypatch):
|
||||
profile, _ = make_profile(tmp_path)
|
||||
monkeypatch.setenv("REYNA_THUNDERBIRD_PROFILE", str(profile))
|
||||
|
||||
result = runner.invoke(app, ["email", "search", "Whirlpool Refrigerator", "--json"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
payload = json.loads(result.stdout)
|
||||
assert payload["ok"] is True
|
||||
assert payload["source"] == "local_thunderbird"
|
||||
assert payload["result"]["results"][0]["message_id"] == "<fridge@example.com>"
|
||||
|
||||
|
||||
def test_email_cli_show_by_id(tmp_path, monkeypatch):
|
||||
profile, _ = make_profile(tmp_path)
|
||||
monkeypatch.setenv("REYNA_THUNDERBIRD_PROFILE", str(profile))
|
||||
search = runner.invoke(app, ["email", "search", "WRS321SDHZ", "--json"])
|
||||
message_id = json.loads(search.stdout)["result"]["results"][0]["id"]
|
||||
|
||||
result = runner.invoke(app, ["email", "show", message_id, "--body", "--json"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
payload = json.loads(result.stdout)
|
||||
assert "Whirlpool Refrigerator" in payload["result"]["body"]
|
||||
+123
-3
@@ -3,7 +3,7 @@ from pathlib import Path
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from reyna_cli.cli import app
|
||||
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
|
||||
|
||||
@@ -53,8 +53,16 @@ devices:
|
||||
|
||||
|
||||
def test_config_get_device():
|
||||
registry = Registry(devices=[Device(id="iphone_mcp", url="http://localhost:8080/api/mcp")])
|
||||
assert get_device("iphone", registry).url == "http://localhost:8080/api/mcp"
|
||||
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
|
||||
|
||||
|
||||
@@ -88,12 +96,74 @@ def test_screen_has_subcommands():
|
||||
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
|
||||
@@ -116,3 +186,53 @@ def test_mongo_has_subcommands():
|
||||
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
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from reyna_cli.web import WebSettings, create_app, normalize_args, RunRequest, run_web_server
|
||||
from reyna_cli.tts import TTSError, _custom_tts
|
||||
|
||||
|
||||
def test_web_health_without_auth_config():
|
||||
app = create_app(
|
||||
WebSettings(
|
||||
auth_enabled=True,
|
||||
dev_auth=False,
|
||||
session_secret="",
|
||||
authentik_issuer="https://auth.reynafamily.com/application/o/reyna-cli-web/",
|
||||
client_id=None,
|
||||
client_secret=None,
|
||||
basic_user=None,
|
||||
basic_password=None,
|
||||
public_url=None,
|
||||
command_timeout=5,
|
||||
output_limit=1000,
|
||||
workdir=Path.cwd(),
|
||||
)
|
||||
)
|
||||
client = TestClient(app)
|
||||
response = client.get("/healthz")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["auth_enabled"] is True
|
||||
assert response.json()["oidc_configured"] is False
|
||||
|
||||
|
||||
def test_web_index_renders_with_dev_auth():
|
||||
app = create_app(
|
||||
WebSettings(
|
||||
auth_enabled=True,
|
||||
dev_auth=True,
|
||||
session_secret="dev-secret",
|
||||
authentik_issuer="https://auth.reynafamily.com/application/o/reyna-cli-web/",
|
||||
client_id=None,
|
||||
client_secret=None,
|
||||
basic_user=None,
|
||||
basic_password=None,
|
||||
public_url=None,
|
||||
command_timeout=5,
|
||||
output_limit=1000,
|
||||
workdir=Path.cwd(),
|
||||
)
|
||||
)
|
||||
client = TestClient(app)
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200
|
||||
assert "Reyna CLI Web" in response.text
|
||||
assert "doctor --json" in response.text
|
||||
|
||||
|
||||
def test_web_refuses_dev_auth_on_lan_bind(monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("HERMES_ENV_PATH", str(tmp_path / "missing.env"))
|
||||
monkeypatch.setenv("REYNA_WEB_DEV_AUTH", "1")
|
||||
monkeypatch.delenv("REYNA_WEB_ALLOW_INSECURE_LAN", raising=False)
|
||||
try:
|
||||
run_web_server(host="0.0.0.0", port=8765)
|
||||
except SystemExit as exc:
|
||||
assert "Refusing to run" in str(exc)
|
||||
else:
|
||||
raise AssertionError("LAN dev-auth server was allowed")
|
||||
|
||||
|
||||
def test_web_refuses_lan_bind_without_oidc_config(monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("HERMES_ENV_PATH", str(tmp_path / "missing.env"))
|
||||
monkeypatch.delenv("REYNA_WEB_DEV_AUTH", raising=False)
|
||||
monkeypatch.delenv("REYNA_WEB_AUTH_CLIENT_ID", raising=False)
|
||||
monkeypatch.delenv("REYNA_WEB_AUTH_CLIENT_SECRET", raising=False)
|
||||
monkeypatch.delenv("REYNA_WEB_BASIC_USER", raising=False)
|
||||
monkeypatch.delenv("REYNA_WEB_BASIC_PASSWORD", raising=False)
|
||||
monkeypatch.delenv("REYNA_WEB_SESSION_SECRET", raising=False)
|
||||
try:
|
||||
run_web_server(host="0.0.0.0", port=8765)
|
||||
except SystemExit as exc:
|
||||
assert "without configured authentication" in str(exc)
|
||||
else:
|
||||
raise AssertionError("LAN server without OIDC config was allowed")
|
||||
|
||||
|
||||
def test_web_basic_auth_protects_index():
|
||||
app = create_app(
|
||||
WebSettings(
|
||||
auth_enabled=True,
|
||||
dev_auth=False,
|
||||
session_secret="basic-secret",
|
||||
authentik_issuer="https://auth.reynafamily.com/application/o/reyna-cli-web/",
|
||||
client_id=None,
|
||||
client_secret=None,
|
||||
basic_user="adolfo",
|
||||
basic_password="correct-password",
|
||||
public_url=None,
|
||||
command_timeout=5,
|
||||
output_limit=1000,
|
||||
workdir=Path.cwd(),
|
||||
)
|
||||
)
|
||||
client = TestClient(app)
|
||||
unauthenticated = client.get("/")
|
||||
assert unauthenticated.status_code == 401
|
||||
assert unauthenticated.headers["www-authenticate"] == 'Basic realm="Reyna CLI Web"'
|
||||
|
||||
authenticated = client.get("/", auth=("adolfo", "correct-password"))
|
||||
assert authenticated.status_code == 200
|
||||
assert "HTTP Basic auth" in authenticated.text
|
||||
|
||||
|
||||
def test_web_allows_lan_bind_with_basic_auth(monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("HERMES_ENV_PATH", str(tmp_path / "missing.env"))
|
||||
called = {}
|
||||
|
||||
def fake_run(app_path, host, port, reload):
|
||||
called.update({"app_path": app_path, "host": host, "port": port, "reload": reload})
|
||||
|
||||
monkeypatch.delenv("REYNA_WEB_DEV_AUTH", raising=False)
|
||||
monkeypatch.delenv("REYNA_WEB_AUTH_CLIENT_ID", raising=False)
|
||||
monkeypatch.delenv("REYNA_WEB_AUTH_CLIENT_SECRET", raising=False)
|
||||
monkeypatch.setenv("REYNA_WEB_BASIC_USER", "adolfo")
|
||||
monkeypatch.setenv("REYNA_WEB_BASIC_PASSWORD", "correct-password")
|
||||
monkeypatch.setattr("uvicorn.run", fake_run)
|
||||
|
||||
run_web_server(host="0.0.0.0", port=8765)
|
||||
assert called["host"] == "0.0.0.0"
|
||||
|
||||
|
||||
def test_web_api_run_uses_argument_vector(monkeypatch):
|
||||
app = create_app(
|
||||
WebSettings(
|
||||
auth_enabled=False,
|
||||
dev_auth=False,
|
||||
session_secret="dev-secret",
|
||||
authentik_issuer="https://auth.reynafamily.com/application/o/reyna-cli-web/",
|
||||
client_id=None,
|
||||
client_secret=None,
|
||||
basic_user=None,
|
||||
basic_password=None,
|
||||
public_url=None,
|
||||
command_timeout=5,
|
||||
output_limit=1000,
|
||||
workdir=Path.cwd(),
|
||||
)
|
||||
)
|
||||
|
||||
def fake_run(args, settings):
|
||||
return {"ok": True, "args": args, "exit_code": 0, "stdout": "done", "stderr": ""}
|
||||
|
||||
monkeypatch.setattr("reyna_cli.web.run_reyna_cli", fake_run)
|
||||
client = TestClient(app)
|
||||
response = client.post("/api/run", json={"command": "doctor --json"})
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["ok"] is True
|
||||
assert payload["args"] == ["doctor", "--json"]
|
||||
assert payload["stdout"] == "done"
|
||||
|
||||
|
||||
def test_normalize_args_refuses_nested_server():
|
||||
try:
|
||||
normalize_args(RunRequest(command="--help web"))
|
||||
except Exception as exc:
|
||||
assert getattr(exc, "status_code", None) == 400
|
||||
else:
|
||||
raise AssertionError("nested web command was not rejected")
|
||||
|
||||
|
||||
def test_custom_tts_rejects_raw_text_placeholder(tmp_path):
|
||||
try:
|
||||
_custom_tts("hello", tmp_path / "audio", "printf {raw_text}", None)
|
||||
except TTSError as exc:
|
||||
assert "raw_text" in str(exc)
|
||||
else:
|
||||
raise AssertionError("raw_text placeholder was not rejected")
|
||||
Reference in New Issue
Block a user