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