feat(privacy-host): add signed native calendar contacts and reminders host
Add the owner-only AF_UNIX Reyna CLI privacy host, strict signed-app installation, and typed native routing for Calendar, Contacts, and Reminders.\n\nAdd bounded system-status paths and config-only direct local-service wrappers. Preserve MacMiniMCP pending explicit cutover approval.\n\nApple Notes is intentionally deferred: no native Notes operations, Apple Events declaration, or Automation helper are included; legacy Notes handling remains untouched.
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
"""Tests for calendar event list/create native wrappers and CLI routing – TDD, no live calls."""
|
||||
|
||||
from pathlib import Path
|
||||
import json
|
||||
import pytest
|
||||
from typer.testing import CliRunner
|
||||
from reyna_cli.cli import app
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def test_native_calendar_events_list_success(monkeypatch):
|
||||
from reyna_cli import privacy_host as ph_mod
|
||||
|
||||
captured = {}
|
||||
|
||||
class FakeClient:
|
||||
def call(self, op, args):
|
||||
captured["op"] = op
|
||||
captured["args"] = args
|
||||
return {"id": "abc", "ok": True, "result": {"protocol_version": "1.0.0", "operation": "calendar.events.list", "events": []}}
|
||||
|
||||
monkeypatch.setattr(ph_mod, "PrivacyClient", FakeClient)
|
||||
|
||||
result = ph_mod.native_calendar_events_list(start="2026-01-01T00:00:00Z", end="2026-01-02T00:00:00Z", calendar="Home", limit=25)
|
||||
|
||||
assert captured["op"] == "calendar.events.list"
|
||||
assert captured["args"]["start"] == "2026-01-01T00:00:00Z"
|
||||
assert captured["args"]["end"] == "2026-01-02T00:00:00Z"
|
||||
assert captured["args"]["calendar"] == "Home"
|
||||
assert captured["args"]["limit"] == 25
|
||||
assert result["ok"] is True
|
||||
assert result["source"] == "native_privacy_host"
|
||||
|
||||
|
||||
def test_native_calendar_events_list_with_calendar_id(monkeypatch):
|
||||
from reyna_cli import privacy_host as ph_mod
|
||||
|
||||
captured = {}
|
||||
|
||||
class FakeClient:
|
||||
def call(self, op, args):
|
||||
captured["args"] = args
|
||||
return {"id": "abc", "ok": True, "result": {"events": []}}
|
||||
|
||||
monkeypatch.setattr(ph_mod, "PrivacyClient", FakeClient)
|
||||
|
||||
ph_mod.native_calendar_events_list(start="2026-01-01T00:00:00Z", end="2026-01-02T00:00:00Z", calendar_id="stable-id-123", limit=50)
|
||||
|
||||
assert captured["args"]["calendar_id"] == "stable-id-123"
|
||||
assert "calendar" not in captured["args"] or captured["args"].get("calendar") is None
|
||||
|
||||
|
||||
def test_native_calendar_events_list_failure_surfaces(monkeypatch):
|
||||
from reyna_cli import privacy_host as ph_mod
|
||||
from reyna_cli.privacy_client import PrivacyClientError
|
||||
|
||||
class FakeFail:
|
||||
def call(self, op, args):
|
||||
raise PrivacyClientError("privacy RPC returned ok=false: permission_required")
|
||||
|
||||
monkeypatch.setattr(ph_mod, "PrivacyClient", FakeFail)
|
||||
|
||||
with pytest.raises(PrivacyClientError):
|
||||
ph_mod.native_calendar_events_list(start="2026-01-01T00:00:00Z", end="2026-01-02T00:00:00Z")
|
||||
|
||||
|
||||
def test_native_calendar_event_create_success(monkeypatch):
|
||||
from reyna_cli import privacy_host as ph_mod
|
||||
|
||||
captured = {}
|
||||
|
||||
class FakeClient:
|
||||
def call(self, op, args):
|
||||
captured["op"] = op
|
||||
captured["args"] = args
|
||||
return {"id": "c", "ok": True, "result": {"protocol_version": "1.0.0", "operation": "calendar.event.create", "event": {"id": "new"}}}
|
||||
|
||||
monkeypatch.setattr(ph_mod, "PrivacyClient", FakeClient)
|
||||
|
||||
result = ph_mod.native_calendar_event_create(
|
||||
title="Meeting",
|
||||
start="2026-01-01T10:00:00Z",
|
||||
end="2026-01-01T11:00:00Z",
|
||||
all_day=False,
|
||||
notes="bring docs",
|
||||
location="Room 1",
|
||||
calendar_id="cal-id-1",
|
||||
calendar=None,
|
||||
)
|
||||
|
||||
assert captured["op"] == "calendar.event.create"
|
||||
assert captured["args"]["title"] == "Meeting"
|
||||
assert captured["args"]["start"] == "2026-01-01T10:00:00Z"
|
||||
assert captured["args"]["end"] == "2026-01-01T11:00:00Z"
|
||||
assert captured["args"]["all_day"] is False
|
||||
assert captured["args"]["notes"] == "bring docs"
|
||||
assert captured["args"]["calendar_id"] == "cal-id-1"
|
||||
assert result["ok"] is True
|
||||
|
||||
|
||||
def test_native_calendar_event_create_with_calendar_title(monkeypatch):
|
||||
from reyna_cli import privacy_host as ph_mod
|
||||
|
||||
captured = {}
|
||||
|
||||
class FakeClient:
|
||||
def call(self, op, args):
|
||||
captured["args"] = args
|
||||
return {"id": "c", "ok": True, "result": {"event": {"id": "new"}}}
|
||||
|
||||
monkeypatch.setattr(ph_mod, "PrivacyClient", FakeClient)
|
||||
|
||||
ph_mod.native_calendar_event_create(title="T", start="2026-01-01T10:00:00Z", end="2026-01-01T11:00:00Z", calendar="Home")
|
||||
|
||||
assert captured["args"]["calendar"] == "Home"
|
||||
assert "calendar_id" not in captured["args"]
|
||||
|
||||
|
||||
def test_no_mcp_in_new_wrappers():
|
||||
from reyna_cli import privacy_host as ph_mod
|
||||
|
||||
src = Path(ph_mod.__file__).read_text()
|
||||
# wrappers must still not contain MCP fallback
|
||||
assert "call_macmini_tool" not in src
|
||||
assert "MCPClient" not in src
|
||||
|
||||
|
||||
def test_cli_events_uses_native_wrapper(monkeypatch):
|
||||
calls = {"count": 0, "args": None}
|
||||
|
||||
def fake_events(start, end, calendar_id=None, calendar=None, limit=50):
|
||||
calls["count"] += 1
|
||||
calls["args"] = {"start": start, "end": end, "calendar_id": calendar_id, "calendar": calendar, "limit": limit}
|
||||
return {"ok": True, "source": "native_privacy_host", "result": {"events": []}}
|
||||
|
||||
monkeypatch.setattr("reyna_cli.privacy_host.native_calendar_events_list", fake_events)
|
||||
|
||||
result = runner.invoke(app, ["macmini", "calendar", "events", "2026-01-01T00:00:00Z", "2026-01-02T00:00:00Z", "--json"])
|
||||
assert result.exit_code == 0, result.stdout + result.stderr
|
||||
payload = json.loads(result.stdout)
|
||||
assert payload["ok"] is True
|
||||
assert calls["count"] == 1
|
||||
assert calls["args"]["start"] == "2026-01-01T00:00:00Z"
|
||||
|
||||
|
||||
def test_cli_events_with_calendar_title(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
def fake_events(start, end, calendar_id=None, calendar=None, limit=50):
|
||||
captured["calendar"] = calendar
|
||||
captured["calendar_id"] = calendar_id
|
||||
return {"ok": True, "source": "native_privacy_host", "result": {"events": []}}
|
||||
|
||||
monkeypatch.setattr("reyna_cli.privacy_host.native_calendar_events_list", fake_events)
|
||||
|
||||
result = runner.invoke(app, ["macmini", "calendar", "events", "2026-01-01T00:00:00Z", "2026-01-02T00:00:00Z", "--calendar", "Home", "--json"])
|
||||
assert result.exit_code == 0, result.stdout + result.stderr
|
||||
assert captured["calendar"] == "Home"
|
||||
assert captured["calendar_id"] is None
|
||||
|
||||
|
||||
def test_cli_events_with_calendar_index_resolves_once(monkeypatch):
|
||||
# Track how many times native_calendar_list is called – must be exactly once for index compat
|
||||
list_calls = {"count": 0}
|
||||
events_calls = {"args": None}
|
||||
|
||||
def fake_list():
|
||||
list_calls["count"] += 1
|
||||
return {
|
||||
"ok": True,
|
||||
"source": "native_privacy_host",
|
||||
"result": {
|
||||
"protocol_version": "1.0.0",
|
||||
"operation": "calendar.list",
|
||||
"calendars": [
|
||||
{"id": "id-0", "title": "Home", "source": "iCloud", "type": "caldav"},
|
||||
{"id": "id-1", "title": "Work", "source": "iCloud", "type": "caldav"},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
def fake_events(start, end, calendar_id=None, calendar=None, limit=50):
|
||||
events_calls["args"] = {"calendar_id": calendar_id, "calendar": calendar}
|
||||
return {"ok": True, "source": "native_privacy_host", "result": {"events": []}}
|
||||
|
||||
monkeypatch.setattr("reyna_cli.privacy_host.native_calendar_list", fake_list)
|
||||
monkeypatch.setattr("reyna_cli.privacy_host.native_calendar_events_list", fake_events)
|
||||
|
||||
result = runner.invoke(app, ["macmini", "calendar", "events", "2026-01-01T00:00:00Z", "2026-01-02T00:00:00Z", "--calendar-index", "1", "--json"])
|
||||
assert result.exit_code == 0, result.stdout + result.stderr
|
||||
assert list_calls["count"] == 1, "must resolve calendar list exactly once"
|
||||
assert events_calls["args"]["calendar_id"] == "id-1"
|
||||
assert events_calls["args"]["calendar"] is None
|
||||
|
||||
|
||||
def test_cli_events_invalid_calendar_index_returns_error(monkeypatch):
|
||||
def fake_list():
|
||||
return {
|
||||
"ok": True,
|
||||
"source": "native_privacy_host",
|
||||
"result": {
|
||||
"calendars": [
|
||||
{"id": "id-0", "title": "Home", "source": "iCloud", "type": "caldav"},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
monkeypatch.setattr("reyna_cli.privacy_host.native_calendar_list", fake_list)
|
||||
monkeypatch.setattr("reyna_cli.privacy_host.native_calendar_events_list", lambda **kwargs: {"ok": True, "source": "native", "result": {}})
|
||||
|
||||
result = runner.invoke(app, ["macmini", "calendar", "events", "2026-01-01T00:00:00Z", "2026-01-02T00:00:00Z", "--calendar-index", "5", "--json"])
|
||||
assert result.exit_code != 0
|
||||
# fail() with json_output should emit ok:false payload
|
||||
assert "out of range" in result.stdout or "out of range" in result.stderr or "ok" in result.stdout.lower()
|
||||
|
||||
|
||||
def test_cli_create_uses_native_wrapper(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
def fake_create(title, start, end, all_day=False, notes=None, location=None, calendar_id=None, calendar=None):
|
||||
captured["title"] = title
|
||||
captured["calendar"] = calendar
|
||||
captured["calendar_id"] = calendar_id
|
||||
captured["notes"] = notes
|
||||
return {"ok": True, "source": "native_privacy_host", "result": {"event": {"id": "new"}}}
|
||||
|
||||
monkeypatch.setattr("reyna_cli.privacy_host.native_calendar_event_create", fake_create)
|
||||
|
||||
result = runner.invoke(app, ["macmini", "calendar", "create", "Meeting", "2026-01-01T10:00:00Z", "2026-01-01T11:00:00Z", "--calendar", "Home", "--notes", "bring docs", "--json"])
|
||||
assert result.exit_code == 0, result.stdout + result.stderr
|
||||
assert captured["title"] == "Meeting"
|
||||
assert captured["calendar"] == "Home"
|
||||
assert captured["notes"] == "bring docs"
|
||||
|
||||
|
||||
def test_cli_create_with_calendar_index(monkeypatch):
|
||||
list_calls = {"count": 0}
|
||||
create_calls = {}
|
||||
|
||||
def fake_list():
|
||||
list_calls["count"] += 1
|
||||
return {
|
||||
"ok": True,
|
||||
"source": "native_privacy_host",
|
||||
"result": {"calendars": [{"id": "id-xyz", "title": "Home", "source": "iCloud", "type": "caldav"}]},
|
||||
}
|
||||
|
||||
def fake_create(title, start, end, all_day=False, notes=None, location=None, calendar_id=None, calendar=None):
|
||||
create_calls["calendar_id"] = calendar_id
|
||||
create_calls["calendar"] = calendar
|
||||
return {"ok": True, "source": "native_privacy_host", "result": {"event": {"id": "new"}}}
|
||||
|
||||
monkeypatch.setattr("reyna_cli.privacy_host.native_calendar_list", fake_list)
|
||||
monkeypatch.setattr("reyna_cli.privacy_host.native_calendar_event_create", fake_create)
|
||||
|
||||
result = runner.invoke(app, ["macmini", "calendar", "create", "T", "2026-01-01T10:00:00Z", "2026-01-01T11:00:00Z", "--calendar-index", "0", "--json"])
|
||||
assert result.exit_code == 0, result.stdout + result.stderr
|
||||
assert list_calls["count"] == 1
|
||||
assert create_calls["calendar_id"] == "id-xyz"
|
||||
assert create_calls["calendar"] is None
|
||||
|
||||
|
||||
def test_cli_create_no_default_calendar_first_arbitrary(monkeypatch):
|
||||
# When no calendar specified, wrapper should receive None for both and then host will reject (no default)
|
||||
captured = {}
|
||||
|
||||
def fake_create(title, start, end, all_day=False, notes=None, location=None, calendar_id=None, calendar=None):
|
||||
captured["calendar_id"] = calendar_id
|
||||
captured["calendar"] = calendar
|
||||
return {"ok": True, "source": "native_privacy_host", "result": {"event": {"id": "new"}}}
|
||||
|
||||
monkeypatch.setattr("reyna_cli.privacy_host.native_calendar_event_create", fake_create)
|
||||
|
||||
result = runner.invoke(app, ["macmini", "calendar", "create", "Title", "2026-01-01T10:00:00Z", "2026-01-01T11:00:00Z", "--json"])
|
||||
assert result.exit_code == 0, result.stdout + result.stderr
|
||||
assert captured["calendar_id"] is None
|
||||
assert captured["calendar"] is None
|
||||
|
||||
|
||||
def test_cli_events_no_mcp_tool_call(monkeypatch):
|
||||
# Prove no call_macmini_tool present in file after change
|
||||
from reyna_cli import cli as cli_mod
|
||||
|
||||
src = Path(cli_mod.__file__).read_text()
|
||||
# Find events function section – must not contain call_macmini_tool for calendar_list_events
|
||||
# Overall file still may have call_macmini_tool for contacts etc, but our two commands must not use it
|
||||
# So check that native wrappers are used
|
||||
assert "native_calendar_events_list" in src
|
||||
assert "native_calendar_event_create" in src
|
||||
# Ensure the old patterns are gone from those specific functions by checking surrounding lines
|
||||
# Simpler: assert the literal string call_macmini_tool("calendar_list_events" not present
|
||||
assert 'calendar_list_events' not in src or 'call_macmini_tool(\"calendar_list_events\"' not in src
|
||||
assert 'calendar_create_event' not in src or 'call_macmini_tool(\"calendar_create_event\"' not in src
|
||||
Reference in New Issue
Block a user