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:
Adolfo Reyna
2026-08-03 20:27:54 -04:00
parent 6e2117188e
commit 9fd04b0ce4
56 changed files with 14239 additions and 50 deletions
@@ -0,0 +1,443 @@
import XCTest
@testable import ReynaCLIHostCore
import Foundation
// Tests for reminders.request_full_access – TDD, fake providers only
// Modeled after CalendarAuthorizationTests.swift
final class RemindersAuthorizationTests: XCTestCase {
// MARK: - Fake auth providers
struct AlreadyAuthorizedProvider: RemindersAuthorizationProviding {
func authorizationStatus() -> RemindersAuthorizationStatus { .authorized }
func requestFullAccess() throws -> Bool {
XCTFail("requestFullAccess must not be called when already authorized")
return false
}
}
struct NotDeterminedGrantedProvider: RemindersAuthorizationProviding {
func authorizationStatus() -> RemindersAuthorizationStatus { .notDetermined }
func requestFullAccess() throws -> Bool { true }
}
struct NotDeterminedDeniedProvider: RemindersAuthorizationProviding {
func authorizationStatus() -> RemindersAuthorizationStatus { .notDetermined }
func requestFullAccess() throws -> Bool { false }
}
struct DeniedProvider: RemindersAuthorizationProviding {
func authorizationStatus() -> RemindersAuthorizationStatus { .denied }
func requestFullAccess() throws -> Bool { false }
}
struct TimeoutProvider: RemindersAuthorizationProviding {
func authorizationStatus() -> RemindersAuthorizationStatus { .notDetermined }
func requestFullAccess() throws -> Bool {
throw RemindersProviderError.unavailable("reminders authorization timed out")
}
}
struct ErrorProvider: RemindersAuthorizationProviding {
func authorizationStatus() -> RemindersAuthorizationStatus { .notDetermined }
func requestFullAccess() throws -> Bool {
throw RemindersProviderError.unavailable("disk error")
}
}
// MARK: - Empty reminders providers for dispatch
struct EmptyLists: RemindersListsProviding {
func listReminderLists() throws -> [ReminderListItem] { [] }
}
struct EmptyList: RemindersListProviding {
func listReminders(listId: String?, listTitle: String?, completed: Bool?, limit: Int) throws -> [ReminderItem] { [] }
}
struct EmptyCreate: RemindersCreateProviding {
func createReminder(title: String, listId: String?, listTitle: String?, notes: String?, due: Date?, priority: Int?) throws -> ReminderCreateResult {
ReminderCreateResult(id: "r1", list_id: "l1", list_title: "t", title: title)
}
}
struct EmptyCalList: CalendarListProviding { func listCalendars() throws -> [CalendarListItem] { [] } }
struct EmptyCalEvents: CalendarEventsListProviding {
func listEvents(start: Date, end: Date, calendarId: String?, calendarTitle: String?, limit: Int) throws -> [CalendarEventItem] { [] }
}
struct EmptyCalCreate: CalendarEventCreateProviding {
func createEvent(title: String, start: Date, end: Date, allDay: Bool, notes: String?, location: String?, calendarId: String?, calendarTitle: String?) throws -> CalendarEventItem {
CalendarEventItem(id: "e", title: title, start: "2026-01-01T00:00:00Z", end: "2026-01-01T01:00:00Z", all_day: false, calendar_id: "c", calendar_title: "t", notes: nil, location: nil)
}
}
struct MockCalAuth: CalendarAuthorizationProviding {
var status: CalendarAuthorizationStatus = .authorized
func authorizationStatus() -> CalendarAuthorizationStatus { status }
func requestFullAccess() throws -> Bool { false }
}
struct EmptyContactsAuth: ContactsAuthorizationProviding {
func authorizationStatus() -> ContactsAuthorizationStatus { .authorized }
func requestAccess() throws -> Bool { false }
}
struct EmptyContactsSearch: ContactsSearchProviding { func searchContacts(query: String?, limit: Int) throws -> [ContactListItem] { [] } }
struct EmptyContactsRead: ContactsReadProviding {
func readContact(id: String) throws -> ContactDetailItem { throw ContactsProviderError.notFound("nf") }
}
struct EmptyContactsCreate: ContactsCreateProviding {
func createContact(firstName: String?, lastName: String?, organization: String?, jobTitle: String?, note: String?, email: ContactEmailLabelValue?, phone: ContactPhoneLabelValue?) throws -> ContactCreateResult {
throw ContactsProviderError.unavailable("na")
}
}
private func dispatchRemindersAuth(op: String, id: String, auth: RemindersAuthorizationProviding) -> Response {
let req = Request(id: id, operation: op, arguments: .object([:]))
return dispatch(
request: req,
calendarProvider: EmptyCalList(),
eventsProvider: EmptyCalEvents(),
createProvider: EmptyCalCreate(),
authProvider: MockCalAuth(),
contactsAuthProvider: EmptyContactsAuth(),
contactsSearchProvider: EmptyContactsSearch(),
contactsReadProvider: EmptyContactsRead(),
contactsCreateProvider: EmptyContactsCreate(),
remindersAuthProvider: auth,
remindersListsProvider: EmptyLists(),
remindersListProvider: EmptyList(),
remindersCreateProvider: EmptyCreate()
)
}
// MARK: - Core auth behavior
func testAlreadyAuthorizedReturnsAuthorizedWithoutRequesting() {
let auth = AlreadyAuthorizedProvider()
let resp = dispatchRemindersAuth(op: "reminders.request_full_access", id: "ra1", auth: auth)
XCTAssertTrue(resp.ok)
XCTAssertEqual(resp.result?.status, "authorized")
XCTAssertEqual(resp.result?.operation, "reminders.request_full_access")
XCTAssertEqual(resp.id, "ra1")
}
func testNotDeterminedReachesRequestPath() {
final class TrackingProvider: RemindersAuthorizationProviding, @unchecked Sendable {
var didRequest = false
var status: RemindersAuthorizationStatus = .notDetermined
func authorizationStatus() -> RemindersAuthorizationStatus { status }
func requestFullAccess() throws -> Bool {
didRequest = true
return true
}
}
let tracking = TrackingProvider()
let resp = dispatchRemindersAuth(op: "reminders.request_full_access", id: "ra2", auth: tracking)
XCTAssertTrue(tracking.didRequest, "requestFullAccess must be called when notDetermined")
XCTAssertTrue(resp.ok)
}
func testGrantedReturnsAuthorizedResult() {
let auth = NotDeterminedGrantedProvider()
let resp = dispatchRemindersAuth(op: "reminders.request_full_access", id: "ra3", auth: auth)
XCTAssertTrue(resp.ok)
XCTAssertEqual(resp.result?.status, "authorized")
XCTAssertEqual(resp.result?.protocol_version, PROTOCOL_VERSION)
XCTAssertNil(resp.error)
XCTAssertNil(resp.result?.reminders, "must not output reminder content")
XCTAssertNil(resp.result?.reminder_lists)
XCTAssertNil(resp.result?.reminder)
}
func testDeniedReturnsPermissionDenied() {
let auth = NotDeterminedDeniedProvider()
let resp = dispatchRemindersAuth(op: "reminders.request_full_access", id: "ra4", auth: auth)
XCTAssertFalse(resp.ok)
XCTAssertEqual(resp.error?.code, "permission_denied")
XCTAssertNotNil(resp.error?.message)
XCTAssertNil(resp.result)
}
func testAlreadyDeniedPathAlsoDenies() {
let auth = DeniedProvider()
let resp = dispatchRemindersAuth(op: "reminders.request_full_access", id: "ra5", auth: auth)
XCTAssertFalse(resp.ok)
XCTAssertEqual(resp.error?.code, "permission_denied")
}
func testTimeoutReturnsRemindersUnavailable() {
let auth = TimeoutProvider()
let resp = dispatchRemindersAuth(op: "reminders.request_full_access", id: "ra6", auth: auth)
XCTAssertFalse(resp.ok)
XCTAssertEqual(resp.error?.code, "reminders_unavailable")
XCTAssertTrue(resp.error?.message.lowercased().contains("timed out") ?? false)
}
func testErrorReturnsRemindersUnavailable() {
let auth = ErrorProvider()
let resp = dispatchRemindersAuth(op: "reminders.request_full_access", id: "ra7", auth: auth)
XCTAssertFalse(resp.ok)
XCTAssertEqual(resp.error?.code, "reminders_unavailable")
}
func testNoReminderDataInAuthResponses() {
let authOk = NotDeterminedGrantedProvider()
let respOk = dispatchRemindersAuth(op: "reminders.request_full_access", id: "ok", auth: authOk)
XCTAssertNil(respOk.result?.reminders)
XCTAssertNil(respOk.result?.reminder_lists)
XCTAssertNil(respOk.result?.reminder)
XCTAssertNil(respOk.result?.created_reminder)
XCTAssertNil(respOk.result?.calendars)
XCTAssertNil(respOk.result?.events)
let authDen = NotDeterminedDeniedProvider()
let respDen = dispatchRemindersAuth(op: "reminders.request_full_access", id: "den", auth: authDen)
XCTAssertNil(respDen.result)
}
// MARK: - list / create must never prompt
func testRemindersListsDoesNotCallAuthRequest() {
final class SpyLists: RemindersListsProviding, @unchecked Sendable {
var called = false
func listReminderLists() throws -> [ReminderListItem] { called = true; return [] }
}
final class SpyAuth: RemindersAuthorizationProviding, @unchecked Sendable {
var didCallStatus = false
var didCallRequest = false
func authorizationStatus() -> RemindersAuthorizationStatus { didCallStatus = true; return .authorized }
func requestFullAccess() throws -> Bool { didCallRequest = true; return false }
}
let lists = SpyLists()
let auth = SpyAuth()
let req = Request(id: "rl-1", operation: "reminders.lists", arguments: .object([:]))
let resp = dispatch(
request: req,
calendarProvider: EmptyCalList(),
eventsProvider: EmptyCalEvents(),
createProvider: EmptyCalCreate(),
authProvider: MockCalAuth(),
contactsAuthProvider: EmptyContactsAuth(),
contactsSearchProvider: EmptyContactsSearch(),
contactsReadProvider: EmptyContactsRead(),
contactsCreateProvider: EmptyContactsCreate(),
remindersAuthProvider: auth,
remindersListsProvider: lists,
remindersListProvider: EmptyList(),
remindersCreateProvider: EmptyCreate()
)
XCTAssertTrue(resp.ok)
XCTAssertFalse(auth.didCallRequest, "reminders.lists must never call requestFullAccess")
XCTAssertTrue(lists.called)
}
func testRemindersListDoesNotCallAuthRequest() {
final class SpyList: RemindersListProviding, @unchecked Sendable {
var called = false
func listReminders(listId: String?, listTitle: String?, completed: Bool?, limit: Int) throws -> [ReminderItem] { called = true; return [] }
}
final class SpyAuth: RemindersAuthorizationProviding, @unchecked Sendable {
var didCallRequest = false
func authorizationStatus() -> RemindersAuthorizationStatus { .authorized }
func requestFullAccess() throws -> Bool { didCallRequest = true; return false }
}
let rl = SpyList()
let auth = SpyAuth()
let req = Request(id: "r-1", operation: "reminders.list", arguments: .object(["limit": .number(10)]))
let resp = dispatch(
request: req,
calendarProvider: EmptyCalList(),
eventsProvider: EmptyCalEvents(),
createProvider: EmptyCalCreate(),
authProvider: MockCalAuth(),
contactsAuthProvider: EmptyContactsAuth(),
contactsSearchProvider: EmptyContactsSearch(),
contactsReadProvider: EmptyContactsRead(),
contactsCreateProvider: EmptyContactsCreate(),
remindersAuthProvider: auth,
remindersListsProvider: EmptyLists(),
remindersListProvider: rl,
remindersCreateProvider: EmptyCreate()
)
XCTAssertTrue(resp.ok)
XCTAssertFalse(auth.didCallRequest, "reminders.list must never trigger authorization request")
XCTAssertTrue(rl.called)
}
func testRemindersCreateDoesNotCallAuthRequest() {
final class SpyCreate: RemindersCreateProviding, @unchecked Sendable {
var called = false
func createReminder(title: String, listId: String?, listTitle: String?, notes: String?, due: Date?, priority: Int?) throws -> ReminderCreateResult {
called = true
return ReminderCreateResult(id: "x", list_id: "l", list_title: "t", title: title)
}
}
final class SpyAuth: RemindersAuthorizationProviding, @unchecked Sendable {
var didCallRequest = false
func authorizationStatus() -> RemindersAuthorizationStatus { .authorized }
func requestFullAccess() throws -> Bool { didCallRequest = true; return false }
}
let create = SpyCreate()
let auth = SpyAuth()
let req = Request(id: "rc-1", operation: "reminders.create", arguments: .object(["title": .string("Buy milk"), "list": .string("Groceries")]))
let resp = dispatch(
request: req,
calendarProvider: EmptyCalList(),
eventsProvider: EmptyCalEvents(),
createProvider: EmptyCalCreate(),
authProvider: MockCalAuth(),
contactsAuthProvider: EmptyContactsAuth(),
contactsSearchProvider: EmptyContactsSearch(),
contactsReadProvider: EmptyContactsRead(),
contactsCreateProvider: EmptyContactsCreate(),
remindersAuthProvider: auth,
remindersListsProvider: EmptyLists(),
remindersListProvider: EmptyList(),
remindersCreateProvider: create
)
XCTAssertTrue(resp.ok)
XCTAssertFalse(auth.didCallRequest, "reminders.create must never trigger authorization request (permission check is via status only in real provider, but here we assert no prompt)")
XCTAssertTrue(create.called)
}
// MARK: - Bridge tests – deterministic pump without real EventKit
final class TestBox<T>: @unchecked Sendable {
var value: T
init(_ v: T) { value = v }
}
func testBridgePumpsMainRunLoopAndDeliversMainQueueCallback() throws {
let exp = expectation(description: "bridge completes")
let grantedBox = TestBox(false)
let errorBox = TestBox<Error?>(nil)
DispatchQueue.main.async {
let bridge = RemindersMainRunLoopBridge()
do {
grantedBox.value = try bridge.requestAccess(timeout: 2) { completion in
Timer.scheduledTimer(withTimeInterval: 0.02, repeats: false) { _ in
completion(true, nil)
}
}
} catch {
errorBox.value = error
}
exp.fulfill()
}
wait(for: [exp], timeout: 5)
XCTAssertNil(errorBox.value, "bridge must not timeout when it pumps main run loop; got \(String(describing: errorBox.value))")
XCTAssertTrue(grantedBox.value, "granted should be true after main-queue callback is pumped")
}
func testBridgeHandlesCompletionExactlyOnce() throws {
let exp = expectation(description: "exactly once")
let resultBox = TestBox(false)
DispatchQueue.main.async {
let bridge = RemindersMainRunLoopBridge()
do {
resultBox.value = try bridge.requestAccess(timeout: 1) { completion in
completion(true, nil)
completion(false, NSError(domain: "should-be-ignored", code: 1))
}
} catch {}
exp.fulfill()
}
wait(for: [exp], timeout: 2)
XCTAssertTrue(resultBox.value, "First completion should win")
}
func testBridgeThreadSafetyForConcurrentCompletion() throws {
let exp = expectation(description: "thread-safe")
let grantedBox = TestBox(false)
let doneBox = TestBox(false)
DispatchQueue.main.async {
let bridge = RemindersMainRunLoopBridge()
do {
grantedBox.value = try bridge.requestAccess(timeout: 1) { completion in
DispatchQueue.global().async { completion(true, nil) }
DispatchQueue.global().async { completion(false, nil) }
}
doneBox.value = true
} catch {}
exp.fulfill()
}
wait(for: [exp], timeout: 2)
XCTAssertTrue(doneBox.value, "bridge must complete even with concurrent completions")
}
func testBridgePropagatesError() throws {
let exp = expectation(description: "error propagation")
let caughtBox = TestBox(false)
DispatchQueue.main.async {
let bridge = RemindersMainRunLoopBridge()
do {
_ = try bridge.requestAccess(timeout: 1) { completion in
completion(false, NSError(domain: "test", code: 2, userInfo: [NSLocalizedDescriptionKey: "fake EK error"]))
}
} catch let err as RemindersProviderError {
if case .unavailable(let msg) = err {
caughtBox.value = msg.contains("fake EK error")
}
} catch {}
exp.fulfill()
}
wait(for: [exp], timeout: 2)
XCTAssertTrue(caughtBox.value, "Error from EK completion must be wrapped as reminders_unavailable")
}
func testBridgeTimeoutReturnsCorrectError() throws {
let exp = expectation(description: "timeout")
let codeBox = TestBox("")
DispatchQueue.main.async {
let bridge = RemindersMainRunLoopBridge()
do {
_ = try bridge.requestAccess(timeout: 0.15) { _ in }
XCTFail("Should have thrown")
} catch let err as RemindersProviderError {
if case .unavailable(let msg) = err {
codeBox.value = msg
}
} catch {}
exp.fulfill()
}
wait(for: [exp], timeout: 2)
XCTAssertTrue(codeBox.value.lowercased().contains("timed out"), "Timeout must produce 'reminders authorization timed out' message, got \(codeBox.value)")
}
// MARK: - Production code location check
func testOnlyOneFileCallsRequestFullAccessToReminders() throws {
let fm = FileManager.default
var cur = URL(fileURLWithPath: fm.currentDirectoryPath)
var dirs: [URL] = []
for _ in 0..<10 {
let cand = cur.appendingPathComponent("native/ReynaCLIHost/Sources/ReynaCLIHostCore")
if fm.fileExists(atPath: cand.path) { dirs.append(cand); break }
let cand2 = cur.appendingPathComponent("Sources/ReynaCLIHostCore")
if fm.fileExists(atPath: cand2.path) { dirs.append(cand2); break }
let cand3 = cur.appendingPathComponent("native/ReynaCLIHost/Sources/ReynaCLIHost")
if fm.fileExists(atPath: cand3.path) { dirs.append(cand3) }
let cand4 = cur.appendingPathComponent("Sources/ReynaCLIHost")
if fm.fileExists(atPath: cand4.path) { dirs.append(cand4) }
if !dirs.isEmpty { break }
cur = cur.deletingLastPathComponent()
}
guard !dirs.isEmpty else {
XCTFail("Could not locate Sources/ReynaCLIHost dir")
return
}
var hits: [String] = []
for srcDir in dirs {
let files = (try? fm.contentsOfDirectory(at: srcDir, includingPropertiesForKeys: nil)) ?? []
for file in files where file.pathExtension == "swift" {
guard let content = try? String(contentsOf: file) else { continue }
if content.contains("requestFullAccessToReminders") {
hits.append(file.lastPathComponent)
}
}
}
let uniqueSorted = Array(Set(hits)).sorted()
XCTAssertEqual(uniqueSorted, ["RemindersAuthorizationProvider.swift"], "requestFullAccessToReminders must only appear in RemindersAuthorizationProvider.swift, found in \(uniqueSorted)")
}
}