import XCTest /// Local fixture + fake notification center/registrar only. No APNs provider or /// system permission dialog is involved in these deterministic UI scenarios. @MainActor final class AgentPushUITests: XCTestCase { let app = XCUIApplication() override func setUp() async throws { continueAfterFailure = false try await control(["action": "reset", "count": 3]) app.launchArguments = ["-sybil.ios.apiBaseURL", "http://127.0.0.1:8799/api", "-sybil.ios.adminToken", "fixture-token"] app.launchEnvironment = ["SYBIL_PUSH_FIXTURE": "1", "SYBIL_PUSH_PERMISSION": "authorized"] } override func tearDown() async throws { app.terminate() } @discardableResult private func control(_ body: [String: Any]) async throws -> [String: Any] { var request = URLRequest(url: URL(string: "http://127.0.0.1:8799/control")!) request.httpMethod = "POST"; request.httpBody = try JSONSerialization.data(withJSONObject: body) request.setValue("application/json", forHTTPHeaderField: "Content-Type") let (data, _) = try await URLSession.shared.data(for: request) return try XCTUnwrap(JSONSerialization.jsonObject(with: data) as? [String: Any]) } private func settings() { if !app.buttons["Settings"].isHittable { XCTAssertTrue(app.buttons["Show sidebar"].waitForExistence(timeout: 20)) app.buttons["Show sidebar"].tap() } XCTAssertTrue(app.buttons["Settings"].waitForExistence(timeout: 5)); app.buttons["Settings"].tap() XCTAssertTrue(app.buttons["agent-push-refresh"].waitForExistence(timeout: 5)) if !app.buttons["agent-push-refresh"].isHittable { app.scrollViews.firstMatch.swipeUp() } } private func capture(_ name: String) { let attachment = XCTAttachment(screenshot: XCUIScreen.main.screenshot()) attachment.name = name; attachment.lifetime = .keepAlways; add(attachment) } private func openAgent() { if !app.buttons["agent-sidebar"].waitForExistence(timeout: 2) || !app.buttons["agent-sidebar"].isHittable { XCTAssertTrue(app.buttons["Show sidebar"].waitForExistence(timeout: 20)); app.buttons["Show sidebar"].tap() } app.buttons["agent-sidebar"].tap() XCTAssertTrue(app.textFields["agent-composer"].waitForExistence(timeout: 5)) } private func ensureDisabled() { if app.buttons["agent-push-disable"].exists { app.buttons["agent-push-disable"].tap() } XCTAssertTrue(app.buttons["agent-push-enable"].waitForExistence(timeout: 5)) } func testExplicitEnableRefreshAndDisable() async throws { app.launch(); settings(); ensureDisabled() let before = try await control(["action": "state"]) let count = (before["pushRequests"] as? [[String: Any]])?.count ?? 0 app.buttons["agent-push-enable"].tap() XCTAssertTrue(app.buttons["agent-push-disable"].waitForExistence(timeout: 5)) let status = app.staticTexts["agent-push-status"] let enabled = NSPredicate(format: "label == %@", "Notifications are enabled.") let enabledExpectation = expectation(for: enabled, evaluatedWith: status) await fulfillment(of: [enabledExpectation], timeout: 8) capture("push-enabled-generic-preview") app.buttons["agent-push-refresh"].tap() let state = try await control(["action": "state"]) let requests = try XCTUnwrap(state["pushRequests"] as? [[String: Any]]) XCTAssertEqual(requests.count, count + 1) XCTAssertEqual(requests.last?["environment"] as? String, "sandbox") app.buttons["agent-push-disable"].tap() XCTAssertTrue(app.buttons["agent-push-enable"].waitForExistence(timeout: 5)) let revoked = try await control(["action": "state"]) XCTAssertEqual(revoked["pushActive"] as? Int, 0) capture("push-disabled-revoked") } func testColdTapRoutesAgentAndPreservesAttachmentCaption() async throws { app.launch() openAgent() let composer = app.textFields["agent-composer"] XCTAssertTrue(composer.waitForExistence(timeout: 5)) composer.tap(); composer.typeText("Push fixture saved caption") app.terminate() app.launchEnvironment["SYBIL_PUSH_TAP"] = "1" app.launch() XCTAssertTrue(composer.waitForExistence(timeout: 20)) XCTAssertTrue((composer.value as? String ?? "").contains("Push fixture saved caption")) XCTAssertTrue(app.buttons["agent-attach"].exists) capture("push-cold-tap-agent-draft-preserved") } func testTapBeforeLoginWaitsThenRoutesAgent() async throws { try await control(["action": "auth-required", "enabled": true]) app.launchArguments = ["-sybil.ios.apiBaseURL", "http://127.0.0.1:8799/api", "-sybil.ios.adminToken", ""] app.launchEnvironment["SYBIL_PUSH_TAP"] = "1" app.launch() XCTAssertTrue(app.buttons["Connect"].waitForExistence(timeout: 15)) XCTAssertFalse(app.textFields["agent-composer"].exists) let state = try await control(["action": "state"]) XCTAssertEqual(state["readCursor"] as? Int, 0) app.secureTextFields.firstMatch.tap(); app.secureTextFields.firstMatch.typeText("fixture-token") app.buttons["Connect"].tap() XCTAssertTrue(app.textFields["agent-composer"].waitForExistence(timeout: 20)) capture("push-pending-auth-routed-after-login") } func testDeniedAndOldServerLeaveAgentUsable() async throws { app.launchEnvironment["SYBIL_PUSH_PERMISSION"] = "denied" app.launch(); settings() XCTAssertTrue(app.buttons["Open iOS Notification Settings"].waitForExistence(timeout: 5)) capture("push-denied-settings") try await control(["action": "push-available", "enabled": false]) app.buttons["agent-push-refresh"].tap() XCTAssertTrue(app.buttons["Show sidebar"].exists); app.buttons["Show sidebar"].tap() app.buttons["agent-sidebar"].tap() XCTAssertTrue(app.textFields["agent-composer"].waitForExistence(timeout: 10)) XCTAssertTrue(app.staticTexts["History message 3: local simulator fixture."].waitForExistence(timeout: 5)) capture("push-old-server-agent-compatible") } func testWarmTapDismissesAttachmentPickerPreservingDraft() async throws { app.launchEnvironment["SYBIL_PUSH_TAP_ON_FOREGROUND"] = "1" app.launch(); openAgent() let composer = app.textFields["agent-composer"] composer.tap(); composer.typeText(" Warm push preserved draft") app.buttons["agent-attach"].tap(); app.buttons["Files"].tap() XCTAssertTrue(app.buttons["Cancel"].firstMatch.waitForExistence(timeout: 5)) XCUIDevice.shared.press(.home); app.activate() XCTAssertTrue(composer.waitForExistence(timeout: 10)) let visible = expectation(for: NSPredicate(format: "hittable == true"), evaluatedWith: composer) await fulfillment(of: [visible], timeout: 5) XCTAssertFalse(app.buttons["Cancel"].firstMatch.exists) XCTAssertTrue((composer.value as? String ?? "").contains("Warm push preserved draft")) capture("push-warm-tap-dismissed-picker-draft-preserved") } }