Files
oai-swift/oAITests/AppleMailServiceTests.swift
T
rune cf82720b88 Fix Sendable warning in AppleMailError by dropping the NSDictionary payload
appleScriptError carried the raw NSDictionary from NSAppleScript's error
out-param, which isn't Sendable and got flagged once the type crossed an
async boundary. Extract just the two fields actually used (errorNumber,
errorMessage) into plain Int/String instead.
2026-08-19 13:53:06 +02:00

174 lines
7.1 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")
}
}