// // 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") } }