2026-10-14 13:34:37 -07:00
import XCTest
import UIKit
2026-10-14 19:51:27 -07:00
import Photos
2026-10-14 13:34:37 -07:00
@ MainActor
final class AgentUITests : 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 . launch ()
openAgent ()
2026-10-14 19:51:27 -07:00
try clearComposer ()
2026-10-14 13:34:37 -07:00
}
2026-10-14 19:51:27 -07:00
override func tearDown () async throws { app . terminate () }
2026-10-14 13:34:37 -07:00
private func openAgent () {
if ! app . buttons [ "agent-sidebar" ]. waitForExistence ( timeout : 2 ) || ! app . buttons [ "agent-sidebar" ]. isHittable {
XCTAssertTrue ( app . buttons [ "Show sidebar" ]. waitForExistence ( timeout : 15 ))
app . buttons [ "Show sidebar" ]. tap ()
}
XCTAssertTrue ( app . buttons [ "agent-sidebar" ]. waitForExistence ( timeout : 5 ))
app . buttons [ "agent-sidebar" ]. tap ()
XCTAssertTrue ( app . textFields [ "agent-composer" ]. waitForExistence ( timeout : 5 ) || app . textViews [ "agent-composer" ]. exists )
}
private var composer : XCUIElement {
app . textFields [ "agent-composer" ]. exists ? app . textFields [ "agent-composer" ] : app . textViews [ "agent-composer" ]
}
2026-10-14 19:51:27 -07:00
private func clearComposer () throws {
guard let existing = composer . value as ? String , ! existing . isEmpty , existing != "Enter Prompt" else { return }
composer . tap (); composer . press ( forDuration : 1 )
let selectAll = app . descendants ( matching : . any ). matching ( NSPredicate ( format : "label == %@" , "Select All" )). firstMatch
if selectAll . waitForExistence ( timeout : 2 ) { selectAll . tap () }
else { composer . typeText ( String ( repeating : XCUIKeyboardKey . rightArrow . rawValue , count : existing . count )) }
composer . typeText ( String ( repeating : XCUIKeyboardKey . delete . rawValue , count : existing . count ))
let value = composer . value as ? String ?? ""
_ = try XCTUnwrap ( value . isEmpty || value == "Enter Prompt" ? true : nil , "Clear the synthetic fixture caption before the next test" )
}
2026-10-14 13:34:37 -07:00
private func send ( _ text : String ) {
composer . tap (); composer . typeText ( text ); app . buttons [ "agent-send" ]. tap ()
}
private func capture ( _ name : String ) {
let attachment = XCTAttachment ( screenshot : XCUIScreen . main . screenshot ()); attachment . name = name ; attachment . lifetime = . keepAlways
add ( attachment )
}
@ discardableResult private func control ( _ value : [ 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 : value )
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 ])
}
2026-10-14 19:51:27 -07:00
private func uploadOriginal ( name : String , mime : String , bytes : Data ) async throws -> [ String : Any ] {
let boundary = "Fixture-" + UUID (). uuidString
let uploadID = UUID (). uuidString
var body = Data ( "-- \( boundary ) \r\n Content-Disposition: form-data; name= \" uploadId \"\r\n\r\n \( uploadID ) \r\n -- \( boundary ) \r\n Content-Disposition: form-data; name= \" file \" ; filename= \" \( name ) \"\r\n Content-Type: \( mime ) \r\n\r\n " . utf8 )
body . append ( bytes ); body . append ( Data ( " \r\n -- \( boundary ) -- \r\n " . utf8 ))
var request = URLRequest ( url : URL ( string : "http://127.0.0.1:8799/api/v1/agent/attachments" ) ! )
request . httpMethod = "POST" ; request . httpBody = body
request . setValue ( "multipart/form-data; boundary= \( boundary ) " , forHTTPHeaderField : "Content-Type" )
request . setValue ( "Bearer fixture-token" , forHTTPHeaderField : "Authorization" )
let ( data , response ) = try await URLSession . shared . data ( for : request )
XCTAssertEqual (( response as ? HTTPURLResponse )?. statusCode , 201 )
let json = try XCTUnwrap ( JSONSerialization . jsonObject ( with : data ) as ? [ String : Any ])
return try XCTUnwrap ( json [ "attachment" ] as ? [ String : Any ])
}
func testAttachmentHistoryDownloadAndReload () async throws {
let original = Data ( "Synthetic original text. <script>plain text only</script>" . utf8 )
let file = try await uploadOriginal ( name : "fixture-original.txt" , mime : "text/plain" , bytes : original )
let image = UIGraphicsImageRenderer ( size : CGSize ( width : 64 , height : 48 )). image { context in
UIColor . systemTeal . setFill (); context . fill ( CGRect ( x : 0 , y : 0 , width : 64 , height : 48 ))
}
let photo = try await uploadOriginal ( name : "fixture-image.png" , mime : "image/png" , bytes : try XCTUnwrap ( image . pngData ()))
let ids = [ try XCTUnwrap ( file [ "id" ] as ? String ), try XCTUnwrap ( photo [ "id" ] as ? String )]
try await control ([ "action" : "attachment-history" , "ids" : ids , "content" : "Synthetic attachments caption" ])
XCTAssertTrue ( app . staticTexts [ "Synthetic attachments caption" ]. waitForExistence ( timeout : 8 ))
XCTAssertTrue ( app . staticTexts [ "fixture-original.txt" ]. exists )
XCTAssertTrue ( app . staticTexts [ "fixture-image.png" ]. exists )
let path = try XCTUnwrap ( file [ "workspacePath" ] as ? String )
XCTAssertTrue ( app . buttons [ "Copy workspace path: " + path ]. exists )
let download = app . buttons [ "agent-download" ]. firstMatch
if ! download . isHittable { app . scrollViews . firstMatch . swipeDown () }
XCTAssertTrue ( download . waitForExistence ( timeout : 5 )); download . tap ()
XCTAssertTrue ( app . buttons [ "Save or share original" ]. waitForExistence ( timeout : 8 ))
XCTAssertTrue ( app . staticTexts . containing ( NSPredicate ( format : "label CONTAINS %@" , "<script>plain text only</script>" )). firstMatch . exists )
capture ( "agent-attachment-original-and-preview" )
try await control ([ "action" : "long-tool" ])
XCTAssertFalse ( app . staticTexts [ "Local tool output" ]. exists )
app . terminate (); app . launch (); openAgent ()
XCTAssertTrue ( app . staticTexts [ "fixture-image.png" ]. waitForExistence ( timeout : 8 ))
XCTAssertTrue ( app . staticTexts [ "fixture-original.txt" ]. exists )
XCTAssertFalse ( app . staticTexts [ "Local tool output" ]. exists )
capture ( "agent-attachment-reloaded" )
// The real app's authenticated content request is recorded by the loopback fixture.
let state = try await control ([ "action" : "state" ])
let log = try XCTUnwrap ( state [ "log" ] as ? [[ String : Any ]])
XCTAssertTrue ( log . contains { ( $0 [ "path" ] as ? String ) == "/api/v1/agent/attachments/ \( ids [ 0 ] ) /content" })
}
func testAttachmentCapabilityAndPickerCancellation () async throws {
XCTAssertTrue ( app . buttons [ "agent-attach" ]. waitForExistence ( timeout : 8 ))
composer . tap (); composer . typeText ( "Preserved attachment draft caption" )
app . buttons [ "agent-attach" ]. tap ()
app . buttons [ "Files" ]. tap ()
XCTAssertTrue ( app . buttons [ "Cancel" ]. firstMatch . waitForExistence ( timeout : 5 ))
app . buttons [ "Cancel" ]. firstMatch . tap ()
XCTAssertEqual ( composer . value as ? String , "Preserved attachment draft caption" )
app . buttons [ "agent-attach" ]. tap (); app . buttons [ "Photo Library" ]. tap ()
XCTAssertTrue ( app . buttons [ "Cancel" ]. firstMatch . waitForExistence ( timeout : 5 ))
app . buttons [ "Cancel" ]. firstMatch . tap ()
XCTAssertEqual ( composer . value as ? String , "Preserved attachment draft caption" )
capture ( "agent-attachment-pickers-cancelled" )
try await control ([ "action" : "capability" , "enabled" : false ])
app . terminate (); app . launch (); openAgent ()
XCTAssertFalse ( app . buttons [ "agent-attach" ]. exists )
XCTAssertEqual ( composer . value as ? String , "Preserved attachment draft caption" )
try clearComposer ()
capture ( "agent-text-only-capability" )
}
func testPhotoSelectionToAttachmentOnlySend () async throws {
try XCTSkipIf ( true , "Manual/device pre-release check: iOS 26.5 XCTest Photos authorization, callback isolation and cleanup are unreliable in this fixture. Selection-to-send is NOT validated; keep the general suite free of photo mutations." )
// Only synthetic assets are touched; no unrelated photo bytes are read.
if PHPhotoLibrary . authorizationStatus ( for : . readWrite ) != . authorized {
// XCTest's synchronous UI waits must not block the authorization request.
let permission = Task . detached { await PHPhotoLibrary . requestAuthorization ( for : . readWrite ) }
try await Task . sleep ( for : . milliseconds ( 300 ))
let allow = XCUIApplication ( bundleIdentifier : "com.apple.springboard" ). buttons . matching (
NSPredicate ( format : "label IN %@" , [ "Allow Full Access" , "Allow Access to All Photos" , "Allow Access" ])). firstMatch
if allow . waitForExistence ( timeout : 5 ) { allow . tap (); _ = await permission . value }
else if PHPhotoLibrary . authorizationStatus ( for : . readWrite ) != . notDetermined { _ = await permission . value }
}
try XCTSkipUnless ( PHPhotoLibrary . authorizationStatus ( for : . readWrite ) == . authorized ,
"Simulator Photos authorization \( PHPhotoLibrary . authorizationStatus ( for : . readWrite ) . rawValue ) is unavailable to \( Bundle . main . bundleIdentifier ?? "test runner" ) " )
// Recover only fixtures from an interrupted test, identified by BOTH our unique
// date and original-filename prefix, before creating this run's asset.
let options = PHFetchOptions ()
options . predicate = NSPredicate ( format : "creationDate == %@" , ISO8601DateFormatter (). date ( from : "2099-12-31T20:00:00Z" ) ! as NSDate )
let previous = PHAsset . fetchAssets ( with : . image , options : options )
let owned = previous . objects ( at : IndexSet ( integersIn : 0. .< previous . count )). filter {
PHAssetResource . assetResources ( for : $0 ). contains { $0 . originalFilename . hasPrefix ( "sybil-attachment-ui-" ) }
}. map ( \ . localIdentifier )
if ! owned . isEmpty { try await removeSyntheticPhotos ( owned ) }
let name = "sybil-attachment-ui-" + UUID (). uuidString + ".png"
let url = FileManager . default . temporaryDirectory . appendingPathComponent ( name )
let photo = UIGraphicsImageRenderer ( size : CGSize ( width : 96 , height : 64 )). image { context in
UIColor . systemPurple . setFill (); context . fill ( CGRect ( x : 0 , y : 0 , width : 96 , height : 64 ))
}
try XCTUnwrap ( photo . pngData ()). write ( to : url )
defer { try ? FileManager . default . removeItem ( at : url ) }
nonisolated ( unsafe ) var assetID : String ?
try await PHPhotoLibrary . shared (). performChanges { @ Sendable in
let creation = PHAssetCreationRequest . forAsset ()
// A unique synthetic date lets us find this asset without enumerating other photos.
creation . creationDate = ISO8601DateFormatter (). date ( from : "2099-12-31T20:00:00Z" )
let options = PHAssetResourceCreationOptions (); options . originalFilename = name
creation . addResource ( with : . photo , fileURL : url , options : options )
assetID = creation . placeholderForCreatedAsset ?. localIdentifier
}
let ownedID = try XCTUnwrap ( assetID )
let identity = XCTAttachment ( string : ownedID ); identity . name = "synthetic-photo-owned-identifier" ; identity . lifetime = . keepAlways ; add ( identity )
addTeardownBlock { try await Self . eraseSyntheticPhotos ([ ownedID ]) }
do { try await exercisePhotoSelection () }
catch {
try await removeSyntheticPhotos ([ ownedID ])
throw error
}
try await removeSyntheticPhotos ([ ownedID ])
}
private func removeSyntheticPhotos ( _ identifiers : [ String ]) async throws {
try await Self . eraseSyntheticPhotos ( identifiers )
let evidence = XCTAttachment ( string : "Removed \( identifiers . count ) synthetic asset(s) by owned identifier; remaining count 0." )
evidence . name = "synthetic-photo-cleanup" ; evidence . lifetime = . keepAlways ; add ( evidence )
}
nonisolated private static func eraseSyntheticPhotos ( _ identifiers : [ String ]) async throws {
try await Task . detached {
let owned = PHAsset . fetchAssets ( withLocalIdentifiers : identifiers , options : nil )
if owned . count > 0 { try await PHPhotoLibrary . shared (). performChanges { PHAssetChangeRequest . deleteAssets ( owned ) } }
let remaining = PHAsset . fetchAssets ( withLocalIdentifiers : identifiers , options : nil ). count
XCTAssertEqual ( remaining , 0 )
print ( "Synthetic photo cleanup: \( identifiers . count ) owned identifier(s), \( remaining ) remaining." )
}. value
}
private func exercisePhotoSelection () async throws {
app . activate ()
XCTAssertFalse ( app . buttons [ "agent-send" ]. isEnabled )
app . buttons [ "agent-attach" ]. tap (); app . buttons [ "Photo Library" ]. tap ()
let cell = app . descendants ( matching : . any ). matching ( NSPredicate ( format :
"label BEGINSWITH %@ AND NOT label CONTAINS %@" , "Photo, December 31, 2099" , ", Photo," )). firstMatch
XCTAssertTrue ( cell . waitForExistence ( timeout : 8 ))
cell . tap ()
app . buttons . matching ( NSPredicate ( format : "label BEGINSWITH %@" , "Add" )). firstMatch . tap ()
let ready = XCTNSPredicateExpectation ( predicate : NSPredicate ( format : "enabled == true" ), object : app . buttons [ "agent-send" ])
await fulfillment ( of : [ ready ], timeout : 15 )
XCTAssertFalse ( app . buttons [ "agent-upload-retry" ]. exists )
capture ( "agent-photo-upload-ready" )
app . buttons [ "agent-send" ]. tap ()
let fallback = app . staticTexts . containing ( NSPredicate ( format : "label BEGINSWITH %@" , "Attached: " )). firstMatch
XCTAssertTrue ( fallback . waitForExistence ( timeout : 12 ))
XCTAssertFalse ( app . buttons [ "agent-retry" ]. exists )
let state = try await control ([ "action" : "state" ])
let requests = try XCTUnwrap ( state [ "requests" ] as ? [ String : [ String : Any ]])
XCTAssertEqual ( requests . count , 1 )
let submission = try XCTUnwrap ( requests . values . first )
XCTAssertEqual (( submission [ "_payload" ] as ? [ Any ])?. first as ? String , "" )
XCTAssertEqual (( submission [ "attachmentIds" ] as ? [ String ])?. count , 1 )
capture ( "agent-photo-attachment-only-canonical" )
app . terminate (); app . launch (); openAgent ()
XCTAssertTrue ( app . staticTexts . containing ( NSPredicate ( format : "label BEGINSWITH %@" , "Attached: " )). firstMatch . waitForExistence ( timeout : 8 ))
XCTAssertFalse ( app . buttons [ "agent-retry" ]. exists )
capture ( "agent-photo-attachment-only-reloaded" )
}
2026-10-14 13:34:37 -07:00
func testSendQueueToolsAndRetry () async throws {
capture ( "agent-history" )
send ( "first fixture" )
send ( "second fixture" )
XCTAssertTrue ( app . staticTexts [ "second fixture" ]. waitForExistence ( timeout : 5 ))
XCTAssertTrue ( app . staticTexts . containing ( NSPredicate ( format : "label CONTAINS %@" , "Finished: second fixture" )). firstMatch . waitForExistence ( timeout : 12 ))
XCTAssertFalse ( app . otherElements [ "agent-typing" ]. exists )
capture ( "agent-replies-keyboard" )
app . swipeDown ()
send ( "retry fixture" )
XCTAssertTrue ( app . buttons [ "agent-retry" ]. waitForExistence ( timeout : 5 ))
capture ( "agent-retry" )
app . buttons [ "agent-retry" ]. tap ()
XCTAssertTrue ( app . staticTexts . containing ( NSPredicate ( format : "label CONTAINS %@" , "Finished: retry fixture" )). firstMatch . waitForExistence ( timeout : 12 ))
send ( "echo wins fixture" )
XCTAssertTrue ( app . staticTexts . containing ( NSPredicate ( format : "label CONTAINS %@" , "Finished: echo wins fixture" )). firstMatch . waitForExistence ( timeout : 12 ))
XCTAssertFalse ( app . buttons [ "agent-retry" ]. exists )
composer . tap (); app . swipeDown ()
2026-10-14 15:31:24 -07:00
XCTAssertFalse ( app . buttons [ "agent-tool-fixture_search" ]. exists )
XCTAssertFalse ( app . buttons [ "agent-tool-fixture_error" ]. exists )
XCTAssertFalse ( app . staticTexts [ "Inspecting the fixture" ]. exists )
XCTAssertFalse ( app . staticTexts [ "Fixture unavailable" ]. exists )
capture ( "agent-hidden-tools" )
2026-10-14 13:34:37 -07:00
let state = try await control ([ "action" : "state" ])
XCTAssertEqual (( state [ "requests" ] as ? [ String : Any ])?. count , 4 )
}
2026-10-14 23:14:53 -07:00
func testBusyProgressAndSameIDMessageUpdate () async throws {
try await control ([ "action" : "progress" , "queued" : 2 , "workers" : 1 , "step" : 7 ])
XCTAssertTrue ( app . staticTexts [ "Sybil is thinking…" ]. waitForExistence ( timeout : 5 ))
XCTAssertTrue ( app . staticTexts [ "2 queued" ]. waitForExistence ( timeout : 5 ))
try await control ([ "action" : "progress" , "queued" : 3 , "workers" : 2 , "step" : 14 ])
XCTAssertTrue ( app . staticTexts [ "3 queued" ]. waitForExistence ( timeout : 5 ))
XCTAssertTrue ( app . staticTexts [ "2 working" ]. exists )
// Same stable ID, changed content: the transcript boundary must update.
try await control ([ "action" : "edit-last" , "content" : "**Revised response** \n\n [Updated reference](http://127.0.0.1:8799)" ])
XCTAssertTrue ( app . staticTexts [ "Revised response" ]. waitForExistence ( timeout : 5 ))
XCTAssertTrue ( app . links [ "Updated reference" ]. exists )
XCTAssertFalse ( app . staticTexts [ "History message 3: local simulator fixture." ]. exists )
XCTAssertTrue ( app . staticTexts [ "Sybil is thinking…" ]. exists )
try await control ([ "action" : "busy" , "value" : false ])
let stopped = XCTNSPredicateExpectation ( predicate : NSPredicate ( format : "exists == false" ), object : app . staticTexts [ "Sybil is thinking…" ])
await fulfillment ( of : [ stopped ], timeout : 5 )
}
2026-10-14 13:34:37 -07:00
func testReduceMotionSend () async throws {
try XCTSkipUnless ( UIAccessibility . isReduceMotionEnabled , "Run with Simulator Reduce Motion enabled" )
send ( "reduce motion fixture" )
XCTAssertTrue ( app . staticTexts . containing ( NSPredicate ( format : "label CONTAINS %@" , "Finished: reduce motion fixture" )). firstMatch . waitForExistence ( timeout : 12 ))
XCTAssertFalse ( app . otherElements [ "agent-typing" ]. exists )
capture ( "agent-reduce-motion" )
}
2026-10-14 15:31:24 -07:00
func testToolResultsStayHiddenLiveAndAfterReload () async throws {
2026-10-14 13:34:37 -07:00
try await control ([ "action" : "long-tool" ])
2026-10-14 15:31:24 -07:00
try await control ([ "action" : "reply" , "content" : "Reply after a hidden tool result" ])
XCTAssertTrue ( app . staticTexts [ "Reply after a hidden tool result" ]. waitForExistence ( timeout : 5 ))
XCTAssertTrue ( app . staticTexts [ "Reply after a hidden tool result" ]. isHittable )
XCTAssertFalse ( app . buttons [ "agent-tool-fixture_long" ]. exists )
XCTAssertFalse ( app . staticTexts [ "Local tool output" ]. exists )
XCTAssertFalse ( app . staticTexts [ "agent-tool-result" ]. exists )
2026-10-14 13:34:37 -07:00
XCTAssertFalse ( app . buttons [ "agent-jump-latest" ]. exists )
2026-10-14 15:31:24 -07:00
capture ( "agent-hidden-tool-live" )
app . terminate (); app . launch (); openAgent ()
XCTAssertTrue ( app . staticTexts [ "Reply after a hidden tool result" ]. waitForExistence ( timeout : 5 ))
XCTAssertFalse ( app . buttons [ "agent-tool-fixture_long" ]. exists )
XCTAssertFalse ( app . staticTexts [ "Local tool output" ]. exists )
XCTAssertFalse ( app . staticTexts [ "agent-tool-result" ]. exists )
capture ( "agent-hidden-tool-history" )
2026-10-14 13:34:37 -07:00
}
func testTabletSidebarAndInactiveChat () async throws {
try XCTSkipUnless ( UIDevice . current . userInterfaceIdiom == . pad , "iPad layout check" )
XCUIDevice . shared . orientation = . landscapeLeft
XCTAssertTrue ( app . buttons [ "agent-sidebar" ]. waitForExistence ( timeout : 5 ))
XCTAssertTrue ( app . buttons [ "agent-sidebar" ]. isHittable )
app . buttons [ "New chat" ]. tap ()
try await control ([ "action" : "busy" , "value" : true ])
try await control ([ "action" : "reply" , "content" : "iPad inactive chat reply" ])
let unread = XCTNSPredicateExpectation ( predicate : NSPredicate ( format : "label CONTAINS %@" , "unread replies" ), object : app . buttons [ "agent-sidebar" ])
await fulfillment ( of : [ unread ], timeout : 5 )
capture ( "agent-ipad-sidebar" )
app . buttons [ "agent-sidebar" ]. tap ()
XCTAssertTrue ( app . staticTexts [ "iPad inactive chat reply" ]. waitForExistence ( timeout : 5 ))
capture ( "agent-ipad-room-before-send" )
send ( "iPad fixture" )
XCTAssertTrue ( app . staticTexts . containing ( NSPredicate ( format : "label CONTAINS %@" , "Finished: iPad fixture" )). firstMatch . waitForExistence ( timeout : 12 ))
capture ( "agent-ipad-room" )
XCUIDevice . shared . orientation = . portrait
}
func testScrollPreservationAndKeyboard () async throws {
app . terminate ()
try await control ([ "action" : "reset" , "count" : 160 ])
app . launch (); openAgent ()
let scroll = app . scrollViews . firstMatch
XCTAssertTrue ( app . staticTexts [ "History message 160: local simulator fixture." ]. isHittable )
scroll . swipeDown ()
scroll . swipeDown ()
XCTAssertTrue ( app . buttons [ "agent-jump-latest" ]. waitForExistence ( timeout : 3 ))
let anchor = try XCTUnwrap ( app . staticTexts . matching ( NSPredicate ( format : "label BEGINSWITH %@" , "History message" )). allElementsBoundByIndex . first { $0 . isHittable })
let before = anchor . frame . minY
try await control ([ "action" : "reply" , "content" : "Reply while reading earlier history" ])
try await Task . sleep ( for : . milliseconds ( 500 ))
XCTAssertEqual ( anchor . frame . minY , before , accuracy : 2 )
capture ( "agent-reading-anchor" )
for _ in 0. .< 25 {
2026-10-16 11:51:07 -07:00
if app . buttons [ "Load earlier messages" ]. exists && app . buttons [ "Load earlier messages" ]. isHittable { break }
2026-10-14 13:34:37 -07:00
scroll . swipeDown ( velocity : . fast )
}
XCTAssertTrue ( app . buttons [ "Load earlier messages" ]. isHittable )
let first = app . staticTexts [ "History message 61: local simulator fixture." ]
let beforeLoad = first . frame . minY
app . buttons [ "Load earlier messages" ]. tap ()
XCTAssertEqual ( first . frame . minY , beforeLoad , accuracy : 3 )
capture ( "agent-older-history-anchor" )
app . buttons [ "agent-jump-latest" ]. tap ()
XCTAssertTrue ( app . staticTexts [ "Reply while reading earlier history" ]. isHittable )
composer . tap (); composer . typeText ( "A multiline draft \n with a second line \n and a third line" )
XCTAssertTrue ( app . staticTexts [ "Reply while reading earlier history" ]. isHittable )
capture ( "agent-multiline-keyboard" )
}
func testBackgroundUnreadReadReconciliationAndReplay () async throws {
app . buttons [ "Show sidebar" ]. tap ()
try await control ([ "action" : "busy" , "value" : true ])
try await control ([ "action" : "reply" , "content" : "Late asynchronous fixture reply" ])
let unread = XCTNSPredicateExpectation ( predicate : NSPredicate ( format : "label CONTAINS %@" , "1 unread replies" ), object : app . buttons [ "agent-sidebar" ])
await fulfillment ( of : [ unread ], timeout : 5 )
capture ( "agent-unread-sidebar" )
let covered = try await control ([ "action" : "state" ])
XCTAssertLessThan ( covered [ "readCursor" ] as ? Int ?? 0 , covered [ "cursor" ] as ? Int ?? 0 )
try await control ([ "action" : "read" ])
let gone = NSPredicate ( format : "NOT (label CONTAINS %@)" , "unread replies" )
let cleared = XCTNSPredicateExpectation ( predicate : gone , object : app . buttons [ "agent-sidebar" ])
await fulfillment ( of : [ cleared ], timeout : 20 )
app . buttons [ "agent-sidebar" ]. tap ()
XCTAssertTrue ( app . staticTexts [ "Late asynchronous fixture reply" ]. waitForExistence ( timeout : 5 ))
try await control ([ "action" : "disconnect" ])
try await control ([ "action" : "reply" , "content" : "Reconnect fixture reply" ])
XCTAssertTrue ( app . staticTexts [ "Reconnect fixture reply" ]. waitForExistence ( timeout : 10 ))
try await control ([ "action" : "malformed" ])
try await control ([ "action" : "reply" , "content" : "Recovered malformed frame" ])
XCTAssertTrue ( app . staticTexts [ "Recovered malformed frame" ]. waitForExistence ( timeout : 12 ))
XCUIDevice . shared . press (. home )
try await control ([ "action" : "reply" , "content" : "Foreground fixture reply" ])
app . activate ()
XCTAssertTrue ( app . staticTexts [ "Foreground fixture reply" ]. waitForExistence ( timeout : 12 ))
XCTAssertEqual ( app . staticTexts . matching ( NSPredicate ( format : "label == %@" , "Foreground fixture reply" )). count , 1 )
capture ( "agent-foreground-replay" )
}
}