feat: migrate Mac mini services into Reyna CLI

This commit is contained in:
Adolfo Reyna
2026-08-16 08:24:45 -04:00
parent 9fd04b0ce4
commit 032bc3a580
26 changed files with 4216 additions and 315 deletions
+74
View File
@@ -0,0 +1,74 @@
import pytest
from unittest.mock import MagicMock, patch
from reyna_cli.apple_llm_client import AppleLLMClient, get_apple_llm_session, apple_llm_close, apple_llm_status
@pytest.fixture
def mock_apple_llm():
with patch("subprocess.Popen") as mock_popen, \
patch("subprocess.run") as mock_run, \
patch("pathlib.Path.write_text"), \
patch("shutil.rmtree"):
# Mock the binary build
mock_run.return_value = MagicMock(returncode=0)
# Mock the process
mock_proc = MagicMock()
mock_proc.poll.return_value = None
mock_proc.pid = 1234
mock_proc.stdout.readline.side_effect = [
'{"id": "0", "ok": true, "text": "Polished text", "ms": 100, "mode": "line"}\\n',
'{"id": "1", "ok": true, "text": "Quick reply", "ms": 50, "mode": "quick_reply"}\\n',
'{"id": "2", "ok": true, "text": "Chat response", "ms": 200, "mode": "chat"}\\n',
'', # EOF
]
mock_popen.return_value = mock_proc
client = AppleLLMClient()
yield client
def test_apple_llm_lifecycle(mock_apple_llm):
# Test status before start
status = apple_llm_status()
assert status["active"] is False
# Start and check status
mock_apple_llm.start()
status = apple_llm_status()
# Note: apple_llm_status uses the global singleton, not the fixture instance.
# For the purpose of these tests, we'll mock the global session if needed,
# but let's focus on the Client logic first.
def test_apple_llm_call_success(mock_apple_llm):
payload = {"mode": "line", "text": "Hello world"}
mock_proc = MagicMock()
mock_apple_llm.proc = mock_proc
def reply_on_write(_: str) -> None:
event, result_box = mock_apple_llm.pending["0"]
result_box["data"] = {"ok": True, "text": "Polished!"}
event.set()
mock_proc.stdin.write.side_effect = reply_on_write
with patch.object(mock_apple_llm, "start"):
res = mock_apple_llm.call(payload)
assert res["ok"] is True
assert res["text"] == "Polished!"
def test_apple_llm_timeout(mock_apple_llm):
with patch("threading.Event.wait", return_value=False):
res = mock_apple_llm.call({"mode": "line", "text": "test"}, timeout=0.1)
assert res["ok"] is False
assert "timeout" in res["error"]
def test_apple_llm_check_mock(mock_apple_llm):
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(
stdout='{"ok": true, "available": true, "ping": "ok"}',
returncode=0
)
res = mock_apple_llm.check()
assert res["ok"] is True
assert res["available"] is True
+142
View File
@@ -0,0 +1,142 @@
import json
from pathlib import Path
from typer.testing import CliRunner
from reyna_cli.cli import app
runner = CliRunner()
def test_media_execution_local_tts_delegates(monkeypatch, tmp_path):
from reyna_cli import media_execution
target = tmp_path / "speech.wav"
def fake_synthesize(text, output, **kwargs):
output.write_bytes(b"RIFFfake")
return output
monkeypatch.setattr(media_execution, "synthesize_wav", fake_synthesize)
result = media_execution.generate_local_tts("hello", target, voice="Alex")
assert result["ok"] is True
assert result["filePath"] == str(target)
assert target.read_bytes() == b"RIFFfake"
def test_media_execution_kokoro_writes_returned_file(monkeypatch, tmp_path):
from reyna_cli import media_execution
source = tmp_path / "source.wav"
source.write_bytes(b"wav")
target = tmp_path / "target.wav"
monkeypatch.setattr(media_execution, "_json_request", lambda *args, **kwargs: {"filePath": str(source), "voice": "af_heart"})
result = media_execution.generate_kokoro("hello", target, url="http://127.0.0.1:7332")
assert result["ok"] is True
assert target.read_bytes() == b"wav"
def test_media_execution_gemini_requires_key(monkeypatch, tmp_path):
from reyna_cli import media_execution
monkeypatch.delenv("GEMINI_API_KEY", raising=False)
try:
media_execution.generate_gemini_image("a tree", tmp_path / "tree.png")
except RuntimeError as exc:
assert "GEMINI_API_KEY" in str(exc)
else:
raise AssertionError("missing Gemini key must fail before network")
def test_media_execution_gemini_decodes_image(monkeypatch, tmp_path):
from reyna_cli import media_execution
monkeypatch.setenv("GEMINI_API_KEY", "test-only")
monkeypatch.setattr(media_execution, "_json_request", lambda *args, **kwargs: {"id": "i1", "output_image": {"data": "aGVsbG8=", "mime_type": "image/png"}})
target = tmp_path / "tree.png"
result = media_execution.generate_gemini_image("a tree", target)
assert result["path"] == str(target)
assert target.read_bytes() == b"hello"
def test_cli_exposes_execution_commands():
for args in [
["local-services", "speech", "--help"],
["local-services", "kokoro", "--help"],
["local-services", "voicebox", "--help"],
["local-services", "image", "--help"],
]:
result = runner.invoke(app, args)
assert result.exit_code == 0, result.stdout
assert "generate" in result.stdout
def test_cli_image_rejects_unknown_engine():
result = runner.invoke(app, ["local-services", "image", "generate", "tree", "--engine", "unknown", "--json"])
assert result.exit_code == 1
assert "codex or gemini" in result.stdout
assert json.loads(result.stdout)["ok"] is False
def test_speech_file_execution_uses_cached_binary(monkeypatch, tmp_path):
from reyna_cli import speech_execution
audio = tmp_path / "sample.wav"
audio.write_bytes(b"wav")
binary = tmp_path / "transcriber"
binary.write_bytes(b"binary")
monkeypatch.setattr(speech_execution, "_ensure_binary", lambda: binary)
monkeypatch.setattr(
speech_execution.subprocess,
"run",
lambda *args, **kwargs: type("Result", (), {"returncode": 0, "stdout": '{"ok":true,"transcript":"hello"}', "stderr": ""})(),
)
result = speech_execution.transcribe_file(audio, locale="en-US")
assert result["transcript"] == "hello"
assert result["source"] == "reyna_cli_direct"
def test_speech_file_execution_rejects_missing_audio(tmp_path):
from reyna_cli.speech_execution import SpeechExecutionError, transcribe_file
try:
transcribe_file(tmp_path / "missing.wav")
except SpeechExecutionError as exc:
assert "not found" in str(exc)
else:
raise AssertionError("missing audio must fail before compilation")
def test_live_session_frames_audio_and_reads_event(monkeypatch, tmp_path):
from reyna_cli import speech_live
class FakeStdin:
def __init__(self):
self.data = bytearray()
def write(self, value):
self.data.extend(value)
def flush(self):
pass
def close(self):
pass
class FakeProcess:
def __init__(self):
self.stdin = FakeStdin()
self.stdout = []
def poll(self):
return None
def wait(self, timeout=None):
return 0
def kill(self):
pass
fake = FakeProcess()
monkeypatch.setattr(speech_live, "_ensure_binary", lambda: tmp_path / "live")
monkeypatch.setattr(speech_live.subprocess, "Popen", lambda *args, **kwargs: fake)
session = speech_live.SpeechLiveSession("en-US")
session.events.put({"ok": True, "event": "final", "text": "hello"})
result = session.transcribe_chunk(b"wav")
assert result["text"] == "hello"
assert bytes(fake.stdin.data[:4]) == b"\x00\x00\x00\x03"
session.close()
+58 -251
View File
@@ -1,268 +1,75 @@
"""Tests for Notes deferred status — no Notes integration may exist.
"""Regression tests for direct mutable Apple Notes CLI support.
Assert:
- no `notes.` protocol ops
- no native_notes wrappers
- no CLI Notes authorization/subcommands
- AppleEvents usage key forbidden and absent from plist/app policy
- no AppKit link/import
- docs correctly say deferred + legacy untouched
Notes are deliberately implemented in Python through a fixed JXA script, not
in the stable Swift privacy host. That preserves the signed launcher binary.
"""
from __future__ import annotations
import plistlib
from pathlib import Path
import re
import json
from types import SimpleNamespace
REPO = Path(__file__).resolve().parents[1]
from typer.testing import CliRunner
from reyna_cli.cli import app
from reyna_cli.notes_direct import create_note, list_notes, read_note
runner = CliRunner()
def test_no_notes_protocol_ops_in_privacy_contract():
from reyna_cli.privacy_contract import ALLOWED_OPERATIONS, _COMMAND_TO_OPERATION
def _success_runner(expected_payload, result):
def run(argv, **kwargs):
assert argv[:5] == ["/usr/bin/osascript", "-l", "JavaScript", "-e", argv[4]]
assert argv[5] == "--"
assert json.loads(argv[6]) == expected_payload
return SimpleNamespace(returncode=0, stdout=json.dumps(result), stderr="")
# No operation may start with notes.
for op in ALLOWED_OPERATIONS.keys():
assert not op.startswith("notes."), f"forbidden notes op {op} still present — Notes is deferred"
# Explicit strings must not appear
forbidden_ops = {"notes.list", "notes.read", "notes.create", "notes.request_access"}
for fo in forbidden_ops:
assert fo not in ALLOWED_OPERATIONS, f"forbidden {fo} present"
for cmd, op in _COMMAND_TO_OPERATION.items():
assert not op.startswith("notes."), f"command {cmd} maps to forbidden notes op {op}"
assert "notes" not in cmd.lower() or "notes" in cmd.lower() and False is False # allow key detection via op only
# also forbid notes_* commands mapping
assert not cmd.startswith("notes_"), f"forbidden notes command {cmd}"
return run
def test_no_native_notes_wrappers():
src = (REPO / "src" / "reyna_cli" / "privacy_host.py").read_text()
assert "native_notes" not in src, "native_notes wrappers must be removed — Notes deferred"
assert "NotesProvider" not in src
assert "NotesAuthorization" not in src
# generic notes field as parameter name for calendar/reminders is allowed,
# but operation names notes.list etc are forbidden — already checked by string search above
# Ensure no wrapper defs remain
for name in ["native_notes_list", "native_notes_read", "native_notes_create", "native_notes_request_access"]:
assert name not in src
def test_list_notes_uses_fixed_jxa_and_json_argument():
notes = list_notes(
query="family",
folder="Adolfo",
include_preview=True,
limit=7,
runner=_success_runner(
{"query": "family", "folder": "Adolfo", "includePreview": True, "limit": 7},
[{"id": "n1", "title": "Family", "folder": "Adolfo"}],
),
)
assert notes == [{"id": "n1", "title": "Family", "folder": "Adolfo"}]
def test_no_cli_notes_subcommands():
cli_src = (REPO / "src" / "reyna_cli" / "cli.py").read_text()
# macmini notes subcommands must be gone
assert "macmini_notes" not in cli_src, "macmini_notes commands must be removed"
# CLI must not register a notes typer under macmini
# Check that macmini help mentions notes is gone — we test via source: no notes app registration
# Allow word 'notes' as parameter name (calendar notes, reminder notes) — but not as subcommand registration
# So forbid 'notes' app creation for macmini
# Look for macmini_notes typed list/read/create defs
assert "def macmini_notes" not in cli_src
# Privacy-host notes-authorize must be gone
assert "notes-authorize" not in cli_src
assert "notes_authorize" not in cli_src
# native_notes usage in cli must be gone
assert "native_notes" not in cli_src
# Ensure NotesProvider strings absent
assert "NotesProvider" not in cli_src
def test_read_and_create_notes_use_json_arguments_without_script_interpolation():
note = read_note(
"x-coredata://note-1",
runner=_success_runner(
{"id": "x-coredata://note-1"},
{"id": "x-coredata://note-1", "title": "Existing", "plaintext": "body"},
),
)
assert note["title"] == "Existing"
created = create_note(
"Test <title>",
"One & two\nthree",
folder="Adolfo",
runner=_success_runner(
{"title": "Test <title>", "body": "One & two\nthree", "folder": "Adolfo"},
{"id": "new-1", "title": "Test <title>", "folder": "Adolfo"},
),
)
assert created["id"] == "new-1"
def test_appleevents_usage_forbidden():
from reyna_cli import app_bundle as ab
def test_notes_cli_is_available_at_top_level_and_macmini_alias(monkeypatch):
monkeypatch.setattr("reyna_cli.cli.list_notes", lambda **_: [{"id": "n1", "title": "Test"}])
d = ab.build_app_bundle_info_plist_dict()
assert "NSAppleEventsUsageDescription" not in d, "AppleEvents usage must be forbidden — Notes deferred"
# Also check Xcode source plist
xcode_plist = REPO / "native" / "ReynaCLIHost" / "ReynaCLIHost" / "Info.plist"
assert xcode_plist.exists()
with open(xcode_plist, "rb") as f:
xd = plistlib.load(f)
assert "NSAppleEventsUsageDescription" not in xd, "Xcode Info.plist must not contain AppleEvents"
top_level = runner.invoke(app, ["notes", "list", "--json"])
assert top_level.exit_code == 0
assert json.loads(top_level.stdout)["notes"][0]["id"] == "n1"
# App bundle policy: forbidden key must be flagged
# Validate that forbidden set includes only Calendar, Contacts, Reminders
allowed = {
"NSCalendarsFullAccessUsageDescription",
"NSCalendarsWriteOnlyAccessUsageDescription",
"NSCalendarsUsageDescription",
"NSContactsUsageDescription",
"NSRemindersFullAccessUsageDescription",
}
for k in d.keys():
if k.startswith("NS") and "UsageDescription" in k:
assert k in allowed, f"unexpected usage key {k} — only calendar/contacts/reminders allowed (Notes deferred)"
def test_app_bundle_validator_forbids_appleevents():
from reyna_cli import app_bundle as ab
import tempfile
repo_root = Path(tempfile.mkdtemp()) / "repo"
bundle = repo_root / "native" / "ReynaCLIHost" / "dist" / "Reyna CLI.app"
contents = bundle / "Contents"
macos = contents / "MacOS"
macos.mkdir(parents=True)
exe = macos / "ReynaCLIHost"
exe.write_bytes(b"binary")
exe.chmod(0o755)
class Proc:
returncode = 0
stdout = ""
stderr = "TeamIdentifier=TEAM123\nAuthority=Apple Development: Foo (TEAM123)\n"
def runner_ok(args, cwd=None, **kwargs):
return Proc()
good = ab.build_app_bundle_info_plist_dict()
with open(contents / "Info.plist", "wb") as f:
plistlib.dump(good, f)
res_ok = ab.validate_app_bundle(repo_root=repo_root, runner=runner_ok)
assert res_ok["ok"] is True, f"should accept plist without AppleEvents: {res_ok['errors']}"
bad = dict(good)
bad["NSAppleEventsUsageDescription"] = "Allow automation"
with open(contents / "Info.plist", "wb") as f:
plistlib.dump(bad, f)
res_bad = ab.validate_app_bundle(repo_root=repo_root, runner=runner_ok)
assert res_bad["ok"] is False
assert any("appleevents" in e.lower() or "unexpected" in e.lower() or "forbidden" in e.lower() for e in res_bad["errors"])
def test_no_appkit_link_or_import():
# AppMain and AppEntry must not import AppKit, must not contain AppleEvents
app_main = (REPO / "native" / "ReynaCLIHost" / "ReynaCLIHost" / "AppMain.swift").read_text()
app_entry = (REPO / "native" / "ReynaCLIHost" / "Sources" / "ReynaCLIHostCore" / "AppEntry.swift").read_text()
for txt, label in [(app_main, "AppMain"), (app_entry, "AppEntry")]:
assert "AppKit" not in txt, f"{label} must not import AppKit — Notes deferred, headless"
assert "NSAppleScript" not in txt
assert "NSAppleEvent" not in txt
assert "AppleEvents" not in txt
# Package.swift must not link AppKit
pkg = (REPO / "native" / "ReynaCLIHost" / "Package.swift").read_text()
assert "AppKit" not in pkg
# pbxproj must not contain AppKit, NotesProvider, NotesAuthorization, ForegroundApp, AppleEventsUsageDescription
pbx = (REPO / "native" / "ReynaCLIHost" / "ReynaCLIHost.xcodeproj" / "project.pbxproj").read_text()
assert "AppKit.framework" not in pbx
assert "NotesProvider.swift" not in pbx, "NotesProvider must not be in Xcode project — deferred"
assert "NotesAuthorizationProvider.swift" not in pbx
assert "ForegroundApp" not in pbx
assert "ForegroundAuthorization" not in pbx
assert "NSAppleEventsUsageDescription" not in pbx
# Protocol.swift must not reference notes operations
proto = (REPO / "native" / "ReynaCLIHost" / "Sources" / "ReynaCLIHostCore" / "Protocol.swift").read_text()
assert "notes.list" not in proto.lower()
assert "notes.read" not in proto.lower()
assert "notes.create" not in proto.lower()
assert "notes.request_access" not in proto.lower()
# Generic notes text field for calendar/reminders is allowed, but operation "notes." must not exist
# Check for NotesProvider types
assert "NotesProvider" not in proto
assert "NotesAuthorization" not in proto
assert "NoteListItem" not in proto and "NoteDetailItem" not in proto, "Notes data models must be removed"
def test_no_notes_in_swift_sources():
core_dir = REPO / "native" / "ReynaCLIHost" / "Sources" / "ReynaCLIHostCore"
for p in core_dir.glob("*.swift"):
txt = p.read_text()
low = txt.lower()
# forbid notes. ops
assert "notes.list" not in low
assert "notes.read" not in low
assert "notes.create" not in low
assert "notes.request_access" not in low
# forbid NSAppleEvents and Notes-type providers
assert "nsappleeventsusagedescription" not in low
assert "NotesProvider" not in txt
assert "NotesAuthorization" not in txt
def test_swift_tests_deleted():
tests_dir = REPO / "native" / "ReynaCLIHost" / "Tests" / "ReynaCLIHostTests"
forbidden = ["NotesAuthorizationTests.swift", "NotesOperationsTests.swift", "ForegroundAuthorizationTests.swift"]
for name in forbidden:
assert not (tests_dir / name).exists(), f"{name} must be deleted"
def test_python_notes_tests_deleted():
assert not (REPO / "tests" / "test_foreground_notes_ls_authorize.py").exists()
assert not (REPO / "tests" / "test_notes_authorization_boundary.py").exists()
def test_docs_deferred_legacy_untouched():
matrix_path = REPO / "docs" / "remaining-coverage-matrix.md"
assert matrix_path.exists()
txt = matrix_path.read_text()
low = txt.lower()
# Must say Notes deferred
assert "notes" in low
assert "deferred" in low, "docs must say Notes deferred"
# Must say legacy untouched
assert "legacy" in low
assert "untouched" in low, "docs must say legacy untouched"
# Must not say Notes belongs inside host as active work — should be in deferred section
# Ensure no active plan to add NotesProvider now
# Allow legacy mention but not as A TODO — check that if Notes is mentioned as A belongs, it's qualified as deferred
# Simplest: ensure the doc contains explicit deferred banner
assert "deferred" in txt, "doc must contain deferred word"
# Ensure no forbidden old instructions about adding NotesProvider as immediate work without deferred qualifier
# The deferred table should list Notes as deferred, not as Done A
# We'll just ensure the word deferred appears near Notes line
for line in txt.splitlines():
if "notes" in line.lower() and ("list/read/create" in line.lower() or "notes.js" in line.lower()):
# in that row, must mention deferred or C or out of scope
assert "deferred" in line.lower() or "untouched" in txt.lower(), f"Notes row must mention deferred: {line}"
def test_no_foreground_notes_references():
# Search all source/test tree for forbidden patterns — but allow generic param named notes (calendar/reminder text)
forbidden_exact = [
"NotesProvider",
"NotesAuthorization",
"ForegroundApp",
"foreground-notes",
"NSAppleEventsUsageDescription",
"notes.request_access",
"native_notes",
]
exclude_dirs = {".venv", "__pycache__", ".git", "build", ".build", "DerivedData", "dist"}
for pattern in forbidden_exact:
for path in REPO.rglob("*"):
if not path.is_file():
continue
# skip excluded
if any(part in exclude_dirs for part in path.parts):
continue
# skip backup
if "backup" in path.parts:
continue
# Only check relevant extensions
if path.suffix not in {".py", ".swift", ".plist", ".md", ".pbxproj", ".toml", ".yaml", ".yml"}:
# also check .xcodeproj is dir, pbxproj covered
if path.name != "project.pbxproj":
continue
# Skip this test file itself if pattern is mentioned in test strings — we need to allow self-reference check for patterns inside this file?
# For this file we will skip self to avoid false positive on literal search
if path.name == "test_notes_deferred.py" or "tests" in path.parts:
continue
# Skip docs remaining-coverage — allowed to mention pattern but must also say deferred; we already validated
# However per task: eliminate only actual Notes integration references (do not remove generic calendar/reminder text fields named notes)
# For forbidden patterns search, we strictly forbid integration references in source/test, not docs describing deferred
if "docs/" in str(path) and pattern == "NSAppleEventsUsageDescription":
continue
if path.name == "app_bundle.py" and pattern == "NSAppleEventsUsageDescription":
continue
try:
txt = path.read_text(errors="ignore")
except Exception:
continue
if pattern in txt:
# Allow generic reminder/calendar 'notes' param already excluded by exact list above — so any hit is real integration
# But also allow mention in backup
if "test_notes_deferred" in str(path):
continue
raise AssertionError(f"forbidden pattern '{pattern}' found in {path}")
compatibility_alias = runner.invoke(app, ["macmini", "notes", "list", "--json"])
assert compatibility_alias.exit_code == 0
assert json.loads(compatibility_alias.stdout)["notes"][0]["title"] == "Test"
+58
View File
@@ -0,0 +1,58 @@
import pytest
from typer.testing import CliRunner
from reyna_cli.cli import app
from reyna_cli.qwen_tts_direct import (
JV_SAMPLE_TEXT,
MODEL_ID,
Qwen3TTSDirectClient,
)
runner = CliRunner()
def test_qwen_defaults_match_gitea_voiceagent():
client = Qwen3TTSDirectClient()
assert client.model_id == MODEL_ID
assert client.model_id == "mlx-community/Qwen3-TTS-12Hz-0.6B-Base-bf16"
assert client.ref_audio.name == "jv_voice_sample.wav"
assert client.ref_text == JV_SAMPLE_TEXT
assert client.instruct is None
def test_qwen_config_is_offline_and_reports_jv_asset(tmp_path):
ref_audio = tmp_path / "jv_voice_sample.wav"
ref_audio.write_bytes(b"not-a-real-wav")
client = Qwen3TTSDirectClient(ref_audio=ref_audio)
status = client.config_status()
assert status["source"] == "direct"
assert status["engine"] == "qwen3-tts"
assert status["model_id"] == MODEL_ID
assert status["ref_audio"] == str(ref_audio)
assert status["ref_audio_exists"] is True
assert status["ref_text_configured"] is True
assert status["offline"] is True
def test_qwen_validation_rejects_missing_text_or_reference(tmp_path):
client = Qwen3TTSDirectClient(ref_audio=tmp_path / "missing.wav")
with pytest.raises(ValueError, match="text required"):
client.validate_generate("")
with pytest.raises(FileNotFoundError, match="reference audio"):
client.validate_generate("hello")
def test_qwen_cli_is_registered_alongside_kokoro():
result = runner.invoke(app, ["local-services", "qwen3-tts", "--help"])
assert result.exit_code == 0, result.output
assert "generate" in result.output
assert "config" in result.output
def test_qwen_cli_exposes_instruct_prompt():
result = runner.invoke(app, ["local-services", "qwen3-tts", "generate", "--help"])
assert result.exit_code == 0, result.output
assert "--instruct" in result.output
+9 -15
View File
@@ -1,11 +1,7 @@
"""Tests for remaining coverage migration — fully consolidated no-Notes CLI.
"""Tests for current direct-Notes and native-host coverage boundaries.
This file replaces the old Notes-native tests. Validates:
- privacy contract contains only calendar/contacts/reminders/system/speech/apple_llm (no notes)
- coverage matrix exists and says Notes deferred + legacy untouched
- system info still works via native host
- local-services direct wrappers offline safe (no Notes)
- docs mention Notes deferred
The native privacy contract remains Notes-free. Apple Notes is intentionally a
mutable Python CLI route, so it does not require rebuilding the Swift host.
"""
import json
@@ -24,11 +20,9 @@ def test_coverage_matrix_exists():
p = Path(__file__).parents[1] / "docs" / "remaining-coverage-matrix.md"
assert p.exists(), f"matrix doc missing at {p}"
content = p.read_text()
# Must mention Notes deferred
assert "Notes" in content
assert "deferred" in content.lower(), "matrix must say Notes deferred"
assert "legacy" in content.lower()
assert "untouched" in content.lower()
assert "direct mutable reyna cli" in content.lower()
assert "fixed jxa" in content.lower()
assert "signed bundle" in content.lower()
# ─── Privacy contract — no notes, deferred ────────────────────────────────
@@ -88,12 +82,12 @@ def test_cli_system_info_uses_native(monkeypatch):
assert payload["ok"] is True
# ─── No Notes CLI ─────────────────────────────────────────────────────────
# ─── Direct Notes CLI (outside the native privacy host) ───────────────────
def test_cli_macmini_no_notes_subcommand():
def test_cli_macmini_notes_compatibility_subcommand():
result = runner.invoke(app, ["macmini", "--help"])
assert result.exit_code == 0
assert "notes" not in result.stdout.lower(), f"macmini must not list notes — Notes deferred, got: {result.stdout}"
assert "notes" in result.stdout.lower()
def test_cli_privacy_host_no_notes_authorize():
+1 -9
View File
@@ -164,14 +164,6 @@ def test_devices_laptop_has_subcommands():
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
@@ -217,7 +209,7 @@ def test_macmini_has_subcommands():
assert result.exit_code == 0
assert "calendar" in result.stdout
assert "contacts" in result.stdout
assert "notes" not in result.stdout, "Notes deferred — macmini must not list notes"
assert "notes" in result.stdout
assert "reminders" in result.stdout
assert "deco" in result.stdout
+70
View File
@@ -0,0 +1,70 @@
from pathlib import Path
from types import SimpleNamespace
from typer.testing import CliRunner
from reyna_cli.cli import app
runner = CliRunner()
def test_run_signed_python_invokes_bundle_with_python_and_forwards_args(tmp_path):
from reyna_cli.signed_launcher import run_signed_python
executable = tmp_path / "ReynaCLIHost"
executable.write_text("binary")
calls = []
def fake_run(args, **kwargs):
calls.append((args, kwargs))
return SimpleNamespace(returncode=17)
result = run_signed_python(["local-services", "speech", "generate", "hello"], executable=executable, runner=fake_run)
assert result == 17
assert calls == [(
[str(executable), "--python", "local-services", "speech", "generate", "hello"],
{"check": False},
)]
def test_run_signed_python_rejects_missing_bundle_executable(tmp_path):
from reyna_cli.signed_launcher import SignedLauncherError, run_signed_python
try:
run_signed_python(["doctor"], executable=tmp_path / "missing")
except SignedLauncherError as exc:
assert "signed Reyna CLI app executable" in str(exc)
else:
raise AssertionError("missing signed executable must fail")
def test_signed_command_forwards_unknown_arguments(monkeypatch):
from reyna_cli import signed_launcher
captured = {}
def fake_run(args):
captured["args"] = args
return 0
monkeypatch.setattr(signed_launcher, "run_signed_python", fake_run)
result = runner.invoke(app, ["signed", "local-services", "speech", "generate", "hello", "--voice", "Alex"])
assert result.exit_code == 0, result.stdout
assert captured["args"] == ["local-services", "speech", "generate", "hello", "--voice", "Alex"]
def test_signed_command_reports_missing_signed_bundle(monkeypatch):
from reyna_cli import signed_launcher
def fake_run(args):
raise signed_launcher.SignedLauncherError("signed Reyna CLI app executable is missing")
monkeypatch.setattr(signed_launcher, "run_signed_python", fake_run)
result = runner.invoke(app, ["signed", "doctor"])
assert result.exit_code == 1
assert "signed Reyna CLI app executable is missing" in result.stdout
+45
View File
@@ -0,0 +1,45 @@
from __future__ import annotations
import json
from pathlib import Path
from typer.testing import CliRunner
from reyna_cli.cli import app
from reyna_cli.voice_direct import PocketTTSDirectClient, UnifiedVoiceClient
runner = CliRunner()
def test_unified_voice_config_keeps_three_named_engines_separate(tmp_path):
status = UnifiedVoiceClient(pocket_voice_state=tmp_path / "jv_pocket.pt").config_status()
assert set(status["engines"]) == {"kokoro", "pocket", "qwen3-tts"}
assert status["engines"]["pocket"]["voice_state"] == str(tmp_path / "jv_pocket.pt")
assert status["engines"]["qwen3-tts"]["model_id"] == "mlx-community/Qwen3-TTS-12Hz-0.6B-Base-bf16"
def test_pocket_validation_requires_exact_custom_state(tmp_path):
client = PocketTTSDirectClient(voice_state=tmp_path / "missing.pt")
try:
client.validate_generate("hello")
except FileNotFoundError as exc:
assert "Pocket voice state" in str(exc)
else:
raise AssertionError("missing Pocket state must not silently fall back")
def test_unified_voice_cli_exposes_explicit_engine_selection():
result = runner.invoke(app, ["local-services", "voice", "generate", "--help"])
assert result.exit_code == 0, result.output
assert "--engine" in result.output
assert "kokoro" in result.output
assert "pocket" in result.output
assert "qwen3-tts" in result.output
def test_unified_voice_config_cli_is_offline(monkeypatch):
result = runner.invoke(app, ["local-services", "voice", "config", "--json"])
assert result.exit_code == 0, result.output
assert set(json.loads(result.stdout)["result"]["engines"]) == {"kokoro", "pocket", "qwen3-tts"}