forked from Manuel/meeting-assistant
68 lines
5.9 KiB
Swift
68 lines
5.9 KiB
Swift
import Foundation
|
|
|
|
private enum SelfTestFailure: Error { case failed(String) }
|
|
|
|
private func expect(_ condition: @autoclosure () -> Bool, _ message: String) throws {
|
|
if !condition() { throw SelfTestFailure.failed(message) }
|
|
}
|
|
|
|
func runDesktopControlSelfTests() {
|
|
do {
|
|
let status = try JSONDecoder().decode(RecordingStatus.self, from: Data(#"{"isRecording":false,"state":2,"launchProfile":null}"#.utf8))
|
|
try expect(String(describing: status.state).lowercased() == "summarizing", "numeric status 2 must show summarizing")
|
|
print("PASS numeric recording status")
|
|
let requests = try decodeDesktopRequests(Data(requestFixture.utf8))
|
|
try expect(requests.count == 1 && requests[0].id == "approval-1", "pending request must decode")
|
|
try expect(requests[0].actions == [DesktopUserAction(id: "yes", label: "Yes"), DesktopUserAction(id: "no", label: "No")], "offered actions must be preserved")
|
|
try expect(requests[0].expiresAt.timeIntervalSince1970 == 1_791_022_260, "offset and fractional timestamps must decode")
|
|
print("PASS pending request wire payload")
|
|
var state = DesktopUserRequestState()
|
|
let now = Date(timeIntervalSince1970: 1_791_022_220)
|
|
_ = state.reconcile(requests, now: now)
|
|
try expect(state.notificationsToAnnounce().map(\.id) == ["approval-1"], "new pending request must be announced")
|
|
state.markAnnounced("approval-1")
|
|
_ = state.reconcile(requests, now: now)
|
|
try expect(state.notificationsToAnnounce().isEmpty && state.pending.count == 1, "repeated polling must not redeliver or hide menu requests")
|
|
try expect(state.reconcile([], now: now) == ["approval-1"] && state.pending.isEmpty, "withdrawn request must remove native presentation")
|
|
print("PASS notification reconciliation and menu fallback")
|
|
_ = state.reconcile(requests, now: now)
|
|
try expect(state.beginResponse(baseUrl: "http://127.0.0.1:5090", requestId: "approval-1", action: "not-offered", now: now) == nil, "invalid action must keep request pending")
|
|
guard let response = state.beginResponse(baseUrl: "http://127.0.0.1:5090/", requestId: "approval-1", action: "yes", now: now) else { throw SelfTestFailure.failed("offered action must produce a response") }
|
|
try expect(response.url?.absoluteString == "http://127.0.0.1:5090/desktop/requests/approval-1/respond" && response.httpMethod == "POST", "notification and menu response endpoint")
|
|
let body = try JSONDecoder().decode([String: String].self, from: response.httpBody ?? Data())
|
|
try expect(body == ["action": "yes"], "chosen action must use shared JSON contract")
|
|
try expect(state.beginResponse(baseUrl: "http://127.0.0.1:5090", requestId: "approval-1", action: "yes", now: now) == nil, "same pending action must not submit twice")
|
|
state.completeResponse("approval-1", accepted: false)
|
|
try expect(!state.isResponding("approval-1") && state.pending.count == 1, "failed transport must leave menu fallback actionable")
|
|
try expect(state.beginResponse(baseUrl: "http://127.0.0.1:5090", requestId: "approval-1", action: "yes", now: requests[0].expiresAt) == nil, "expired response must not submit")
|
|
try expect(state.reconcile(requests, now: requests[0].expiresAt) == ["approval-1"] && state.pending.isEmpty, "expired request must be removed locally")
|
|
try expect(state.beginResponse(baseUrl: "http://127.0.0.1:5090", requestId: "approval-1", action: "yes", now: now) == nil, "stale notification must not reconstruct request")
|
|
print("PASS allowed once-only response and stale action rejection")
|
|
_ = state.reconcile(requests, now: now)
|
|
state.completeResponse("approval-1", accepted: true)
|
|
_ = state.reconcile(requests, now: now)
|
|
try expect(state.pending.isEmpty && state.notificationsToAnnounce().isEmpty, "old HTTP snapshot must not resurrect accepted request")
|
|
print("PASS acknowledged response survives stale polling")
|
|
let category = requests[0].notificationCategory()
|
|
let notification = requests[0].notificationRequest()
|
|
try expect(category.actions.map(\.identifier) == ["yes", "no"], "native notification must offer same actions as menu")
|
|
try expect(category.actions.map(\.title) == ["Yes", "No"], "native action labels")
|
|
try expect(notification.content.title == "Associate project?" && notification.content.body == "Add project Delta to this meeting?", "native notification must show pending request text")
|
|
try expect(notification.content.categoryIdentifier == category.identifier && notification.content.userInfo["requestId"] as? String == "approval-1", "native actions must route original request id")
|
|
try expect(category.options.contains(.customDismissAction), "notification dismissal must leave menu decision pending")
|
|
print("PASS actionable native notification payload")
|
|
let windows = try JSONDecoder().decode([DesktopAgentWindow].self, from: Data(#"[{"id":"workflow-1","url":"http://127.0.0.1:5090/desktop/agent-sessions/workflow-1"},{"id":"workflow-2","url":"http://127.0.0.1:5090/desktop/agent-sessions/workflow-2"}]"#.utf8))
|
|
var windowState = DesktopAgentWindowState()
|
|
try expect(windowState.takeUnopened(windows).map(\.id) == ["workflow-1", "workflow-2"], "two approved workflows must request independent windows")
|
|
try expect(windowState.takeUnopened(windows).isEmpty, "polling must not reopen independent windows")
|
|
print("PASS independent agent window wire payload and opening")
|
|
} catch {
|
|
fputs("FAIL desktop controls self-test: \(error)\n", stderr)
|
|
exit(1)
|
|
}
|
|
}
|
|
|
|
private let requestFixture = #"""
|
|
[{"id":"approval-1","kind":"approval","title":"Associate project?","message":"Add project Delta to this meeting?","createdAt":"2026-10-03T12:10:00.1234567+02:00","expiresAt":"2026-10-03T12:11:00+02:00","actions":[{"id":"yes","label":"Yes"},{"id":"no","label":"No"}]}]
|
|
"""#
|