Files
oai-swift/oAITests/AppleMailServiceTests.swift
T
rune 21d598d88a Add a real permission prompt/status for Mail access, matching Personal Data
Settings previously only surfaced an error message after a failed Test
Connection attempt — there was no way to trigger the actual macOS
Automation permission dialog or see live grant/deny status, unlike
Calendar/Contacts/Reminders/Location.

AEDeterminePermissionToAutomateTarget (askUserIfNeeded: false/true) turns
out to provide exactly that: a non-prompting status check and an explicit
prompt-and-wait call, mirroring EKEventStore.authorizationStatus(for:)/
requestFullAccessToEvents(). Mail's Settings row now uses the same
personalDataRow component as Calendar/Contacts — live status badge plus
a real "Request Access" button — instead of a one-off Test-Connection-only
UI. Test Connection stays as a secondary functional check.
2026-08-19 14:01:29 +02:00

192 lines
7.9 KiB
Swift

//
// AppleMailServiceTests.swift
// oAITests
//
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (C) 2026 Rune Olsen
import Testing
import Foundation
@testable import Confab
@Suite("AppleMailService descriptor decoding and formatting")
struct AppleMailServiceTests {
// MARK: - decodeStringList
private func makeListDescriptor(_ items: [String]) -> NSAppleEventDescriptor {
let list = NSAppleEventDescriptor.list()
for (index, item) in items.enumerated() {
list.insert(NSAppleEventDescriptor(string: item), at: index + 1)
}
return list
}
@Test("decodeStringList decodes a flat list of strings")
func decodesFlatList() {
let descriptor = makeListDescriptor(["a", "b", "c"])
#expect(AppleMailService.decodeStringList(descriptor) == ["a", "b", "c"])
}
@Test("decodeStringList returns empty array for an empty list")
func decodesEmptyList() {
let descriptor = makeListDescriptor([])
#expect(AppleMailService.decodeStringList(descriptor) == [])
}
@Test("decodeStringList falls back to a single-element array for a bare string descriptor")
func decodesBareString() {
let descriptor = NSAppleEventDescriptor(string: "hello")
#expect(AppleMailService.decodeStringList(descriptor) == ["hello"])
}
// MARK: - parseSearchResults
@Test("parseSearchResults decodes a list of 7-field message records")
func parsesSearchResults() {
let record1 = makeListDescriptor(["msg-1", "Receipt from Elkjøp", "orders@elkjop.no", "Monday, 1 January 2026", "INBOX", "Personal", "1"])
let record2 = makeListDescriptor(["msg-2", "Another subject", "someone@example.com", "Tuesday, 2 January 2026", "INBOX", "Work", "0"])
let outer = NSAppleEventDescriptor.list()
outer.insert(record1, at: 1)
outer.insert(record2, at: 2)
let results = AppleMailService.parseSearchResults(from: outer)
#expect(results.count == 2)
#expect(results[0].messageId == "msg-1")
#expect(results[0].subject == "Receipt from Elkjøp")
#expect(results[0].from == "orders@elkjop.no")
#expect(results[0].mailbox == "INBOX")
#expect(results[0].account == "Personal")
#expect(results[0].attachmentCount == "1")
#expect(results[1].messageId == "msg-2")
#expect(results[1].attachmentCount == "0")
}
@Test("parseSearchResults returns empty array for an empty outer list")
func parsesEmptySearchResults() {
let outer = NSAppleEventDescriptor.list()
#expect(AppleMailService.parseSearchResults(from: outer) == [])
}
@Test("parseSearchResults skips malformed records with too few fields")
func skipsMalformedRecords() {
let goodRecord = makeListDescriptor(["msg-1", "Subject", "from@example.com", "date", "INBOX", "Account", "0"])
let badRecord = makeListDescriptor(["only", "three", "fields"])
let outer = NSAppleEventDescriptor.list()
outer.insert(badRecord, at: 1)
outer.insert(goodRecord, at: 2)
let results = AppleMailService.parseSearchResults(from: outer)
#expect(results.count == 1)
#expect(results[0].messageId == "msg-1")
}
// MARK: - mapAppleScriptError
@Test("mapAppleScriptError maps -1743 to an Automation-permission message")
func mapsNotAuthorizedError() {
let message = AppleMailService.mapAppleScriptError(number: -1743, message: "Not authorized")
#expect(message.contains("not authorized"))
#expect(message.contains("Automation"))
}
@Test("mapAppleScriptError maps -600 to a Mail-not-available message")
func mapsApplicationNotRunningError() {
let message = AppleMailService.mapAppleScriptError(number: -600, message: "Application isn't running")
#expect(message.contains("not available"))
}
@Test("mapAppleScriptError falls back to a generic formatted message for unknown error numbers")
func mapsGenericError() {
let message = AppleMailService.mapAppleScriptError(number: -1234, message: "Something odd happened")
#expect(message.contains("-1234"))
#expect(message.contains("Something odd happened"))
}
// MARK: - parseArgs
@Test("parseArgs decodes a JSON arguments string")
func parsesArgsJSON() {
let args = AppleMailService.parseArgs("{\"account\":\"work@example.com\",\"subject_contains\":\"Elkjøp\"}")
#expect(args["account"] as? String == "work@example.com")
#expect(args["subject_contains"] as? String == "Elkjøp")
}
@Test("parseArgs returns empty dictionary for invalid JSON")
func parsesInvalidArgsJSON() {
let args = AppleMailService.parseArgs("not json")
#expect(args.isEmpty)
}
// MARK: - hasSearchCriteria
@Test("hasSearchCriteria is false when every criterion is nil or empty")
func rejectsUnscopedSearch() {
#expect(AppleMailService.hasSearchCriteria(subjectContains: nil, fromContains: "", bodyContains: nil, since: nil, before: "") == false)
}
@Test("hasSearchCriteria is true when any single criterion is non-empty")
func acceptsScopedSearch() {
#expect(AppleMailService.hasSearchCriteria(subjectContains: "Elkjøp", fromContains: nil, bodyContains: nil, since: nil, before: nil) == true)
#expect(AppleMailService.hasSearchCriteria(subjectContains: nil, fromContains: nil, bodyContains: nil, since: "2026-01-01", before: nil) == true)
}
// MARK: - approvalSummary
@Test("approvalSummary describes mail_list_accounts")
func summarizesListAccounts() {
let summary = AppleMailService.shared.approvalSummary(forTool: "mail_list_accounts", arguments: "{}")
#expect(summary == "List your Mail.app accounts")
}
@Test("approvalSummary includes account and subject scope for mail_search")
func summarizesSearch() {
let summary = AppleMailService.shared.approvalSummary(
forTool: "mail_search",
arguments: "{\"account\":\"work@example.com\",\"subject_contains\":\"Elkjøp\"}"
)
#expect(summary.contains("work@example.com"))
#expect(summary.contains("Elkjøp"))
}
@Test("approvalSummary for mail_search with no scope still returns a sensible label")
func summarizesUnscopedSearch() {
let summary = AppleMailService.shared.approvalSummary(forTool: "mail_search", arguments: "{}")
#expect(summary == "Search Mail")
}
@Test("approvalSummary describes mail_save_attachment with name and destination")
func summarizesSaveAttachment() {
let summary = AppleMailService.shared.approvalSummary(
forTool: "mail_save_attachment",
arguments: "{\"attachment_name\":\"receipt.pdf\",\"dest_path\":\"/tmp/receipt.pdf\"}"
)
#expect(summary.contains("receipt.pdf"))
#expect(summary.contains("/tmp/receipt.pdf"))
}
@Test("approvalSummary falls back to a generic label for an unknown tool name")
func summarizesUnknownTool() {
let summary = AppleMailService.shared.approvalSummary(forTool: "mail_bogus", arguments: "{}")
#expect(summary == "Perform action: mail_bogus")
}
// MARK: - mapAutomationPermissionStatus
@Test("mapAutomationPermissionStatus maps noErr (0) to granted")
func mapsGrantedStatus() {
#expect(AppleMailService.mapAutomationPermissionStatus(0) == .granted)
}
@Test("mapAutomationPermissionStatus maps -1743 to denied")
func mapsDeniedStatus() {
#expect(AppleMailService.mapAutomationPermissionStatus(-1743) == .denied)
}
@Test("mapAutomationPermissionStatus maps -1744 (would require consent) and other codes to notDetermined")
func mapsNotDeterminedStatus() {
#expect(AppleMailService.mapAutomationPermissionStatus(-1744) == .notDetermined)
#expect(AppleMailService.mapAutomationPermissionStatus(-9999) == .notDetermined)
}
}