"""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