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,175 @@
import XCTest
@testable import ReynaCLIHostCore
import Foundation
// RED tests first: TDD for calendar.list migration
final class CalendarListTests: XCTestCase {
// MARK: - JSONValue safe representation
func testJSONValueRoundTripsObjectArrayPrimitive() throws {
// Expect JSONValue type to support object/array/string/bool/number/null
let json = """
{"id":"1","operation":"calendar.list","arguments":{"filter":"home","limit":2,"nested":{"a":1},"arr":[1,2,null,true],"flag":false}}
"""
let data = json.data(using: .utf8)!
let req = try JSONDecoder().decode(Request.self, from: data)
// arguments should not be empty struct; should retain values
// We test via encoding back and check presence
let encoded = try JSONEncoder().encode(req)
let obj = try JSONSerialization.jsonObject(with: encoded) as! [String: Any]
let args = obj["arguments"] as! [String: Any]
XCTAssertEqual(args["filter"] as? String, "home")
XCTAssertNotNil(args["nested"])
XCTAssertNotNil(args["arr"])
}
func testJSONValueCodableEquatableSendable() throws {
// Verify JSONValue conforms to Codable, Equatable, Sendable (compile-time)
let v1: JSONValue = .object(["a": .number(1)])
let v2: JSONValue = .object(["a": .number(1)])
XCTAssertEqual(v1, v2)
// Codable roundtrip
let data = try JSONEncoder().encode(v1)
let decoded = try JSONDecoder().decode(JSONValue.self, from: data)
XCTAssertEqual(decoded, v1)
}
// MARK: - CalendarListProvider injection & sorting
// Fake provider for tests
struct FakeSuccessProvider: CalendarListProviding {
let calendars: [CalendarListItem]
func listCalendars() throws -> [CalendarListItem] { calendars }
}
func testCalendarListSuccessAndDeterministicSort() throws {
// Unsorted input should be returned sorted by source/title/id
let unsorted = [
CalendarListItem(id: "c", title: "B", source: "iCloud", type: "caldav"),
CalendarListItem(id: "a", title: "A", source: "Local", type: "local"),
CalendarListItem(id: "b", title: "A", source: "iCloud", type: "caldav"),
CalendarListItem(id: "aa", title: "A", source: "iCloud", type: "caldav"),
]
let provider = FakeSuccessProvider(calendars: unsorted)
let req = Request(id: "id-1", operation: "calendar.list", arguments: .object([:]))
let resp = dispatch(request: req, calendarProvider: provider)
XCTAssertTrue(resp.ok)
XCTAssertEqual(resp.id, "id-1")
// The new response/result model should still carry protocol_version/operation/status and calendars
// We decode result payload to check calendars order
// ResultPayload should support generic calendars? We'll check via JSON
let encoded = try JSONEncoder().encode(resp)
let obj = try JSONSerialization.jsonObject(with: encoded) as! [String: Any]
let result = obj["result"] as! [String: Any]
XCTAssertEqual(result["protocol_version"] as? String, PROTOCOL_VERSION)
XCTAssertEqual(result["operation"] as? String, "calendar.list")
XCTAssertEqual(result["status"] as? String, "ok")
let cals = result["calendars"] as! [[String: Any]]
// Deterministic sort: by source, then title, then id (lexicographic, case-sensitive)
// 'L' (76) < 'i' (105) so Local < iCloud
// Expected order: (Local,A,a), (iCloud,A,aa), (iCloud,A,b), (iCloud,B,c)
XCTAssertEqual(cals[0]["id"] as? String, "a")
XCTAssertEqual(cals[1]["id"] as? String, "aa")
XCTAssertEqual(cals[2]["id"] as? String, "b")
XCTAssertEqual(cals[3]["id"] as? String, "c")
// Ensure only allowed fields
for cal in cals {
XCTAssertEqual(Set(cal.keys), Set(["id","title","source","type"]))
}
}
func testCalendarListPermissionRequired() throws {
struct DeniedProvider: CalendarListProviding {
func listCalendars() throws -> [CalendarListItem] {
throw CalendarProviderError.permissionRequired
}
}
let req = Request(id: "perm-1", operation: "calendar.list", arguments: .object([:]))
let resp = dispatch(request: req, calendarProvider: DeniedProvider())
XCTAssertFalse(resp.ok)
XCTAssertEqual(resp.error?.code, "permission_required")
XCTAssertNil(resp.result, "permission_required must not leak calendar content")
}
func testCalendarListProviderFailure() throws {
struct FailProvider: CalendarListProviding {
func listCalendars() throws -> [CalendarListItem] {
throw CalendarProviderError.unavailable("disk error")
}
}
let req = Request(id: "fail-1", operation: "calendar.list", arguments: .object([:]))
let resp = dispatch(request: req, calendarProvider: FailProvider())
XCTAssertFalse(resp.ok)
XCTAssertEqual(resp.error?.code, "calendar_unavailable")
}
func testCalendarListRequestJSONDecodeWithArgumentsObject() throws {
let json = """
{"id":"json-1","operation":"calendar.list","arguments":{"foo":"bar","num":42}}
"""
let data = json.data(using: .utf8)!
let req = try JSONDecoder().decode(Request.self, from: data)
XCTAssertEqual(req.id, "json-1")
// arguments should be parsed and not throw
let provider = FakeSuccessProvider(calendars: [])
let resp = dispatch(request: req, calendarProvider: provider)
XCTAssertTrue(resp.ok, "calendar.list with args object should succeed")
}
func testServiceHealthStillWorksAfterMigration() throws {
let req = Request(id: "health-1", operation: "service.health", arguments: .object([:]))
let resp = dispatch(request: req, calendarProvider: FakeSuccessProvider(calendars: []))
XCTAssertTrue(resp.ok)
XCTAssertEqual(resp.result?.protocol_version, PROTOCOL_VERSION)
XCTAssertEqual(resp.result?.operation, "service.health")
XCTAssertEqual(resp.result?.status, "ok")
}
// MARK: - EventKit provider must be read-only (static check)
func testProductionEventKitProviderNeverRequestsAccess() throws {
// Read the EventKit provider source and ensure it never calls prompt-triggering APIs
// Note: createEvent legitimately mutates (save) – allowed only inside createEvent method.
let fm = FileManager.default
var cur = URL(fileURLWithPath: fm.currentDirectoryPath)
var providerURL: URL? = nil
for _ in 0..<8 {
let cand = cur.appendingPathComponent("Sources/ReynaCLIHost/CalendarProvider.swift")
if fm.fileExists(atPath: cand.path) { providerURL = cand; break }
let candCore = cur.appendingPathComponent("Sources/ReynaCLIHostCore/CalendarProvider.swift")
if fm.fileExists(atPath: candCore.path) { providerURL = candCore; break }
let cand2 = cur.appendingPathComponent("native/ReynaCLIHost/Sources/ReynaCLIHost/CalendarProvider.swift")
if fm.fileExists(atPath: cand2.path) { providerURL = cand2; break }
let cand2Core = cur.appendingPathComponent("native/ReynaCLIHost/Sources/ReynaCLIHostCore/CalendarProvider.swift")
if fm.fileExists(atPath: cand2Core.path) { providerURL = cand2Core; break }
cur = cur.deletingLastPathComponent()
}
guard let url = providerURL, let content = try? String(contentsOf: url) else {
XCTFail("CalendarProvider.swift not found for read-only safety check")
return
}
let lines = content.components(separatedBy: .newlines)
for line in lines {
let l = line.lowercased()
if l.contains("ekeventstore") && l.contains(".request") {
XCTFail("Production provider must not call request methods on EKEventStore: \(line)")
}
}
XCTAssertTrue(content.contains("authorizationStatus"), "Should check authorizationStatus")
// Ensure no remove mutation anywhere
let lower = content.lowercased()
XCTAssertFalse(lower.contains("remove(") && lower.contains("ekevent"), "Should not remove EKEvent")
// No AppleScript
XCTAssertFalse(lower.contains("nsapplescript") || lower.contains("appleevent"), "Should not use AppleScript")
// If save exists, it must be inside createEvent func (mutation allowed only there)
if lower.contains("save(") {
// crude check: ensure save appears after func createEvent
let parts = content.components(separatedBy: "func createEvent")
XCTAssertEqual(parts.count, 2, "save should only appear in createEvent, found multiple or none")
let beforeCreate = parts[0].lowercased()
XCTAssertFalse(beforeCreate.contains("save(") && beforeCreate.contains("ekevent"), "save(EKEvent) must not appear outside createEvent")
}
}
}