2026-10-14 19:51:27 -07:00
import CryptoKit
import Foundation
import ImageIO
import Testing
import UIKit
import UniformTypeIdentifiers
@ testable import Sybil
private let attachmentLimits = AgentAttachmentCapabilities ( version : 1 , maxCount : 8 , maxTotalBytes : 33_554_432 ,
maxImageBytes : 6_291_456 , maxFileBytes : 8_388_608 , imageMimeTypes : [ "image/png" , "image/jpeg" , "image/gif" , "image/webp" ],
maxImagePixels : 50_000_000 , maxImageDimension : 16_384 )
private let originalBytes = Data ([ 0 , 255 , 10 , 13 , 128 , 42 ])
private func sampleAttachment ( _ id : String = "opaque-server-id" , data : Data = originalBytes , name : String = "original.bin" ) -> AgentAttachment {
AgentAttachment ( id : id , kind : . file , filename : name , mimeType : "application/octet-stream" , sizeBytes : data . count ,
sha256 : SHA256 . hash ( data : data ). map { String ( format : "%02x" , $0 ) }. joined (), workspacePath : "/runtime/provided/ \( name ) " )
}
2026-10-20 14:23:58 -07:00
private func descriptorData ( capability : Bool = true , assistantImages : Bool = false ) throws -> Data {
2026-10-14 19:51:27 -07:00
var object : [ String : Any ] = [ "id" : "root" , "title" : "Sybil" , "type" : "persistent-agent" , "model" : "fixture" , "protocol" : "pi-room.v1" , "websocket" : "/v1/agent/ws" ,
"activity" : [ "state" : "idle" , "isThinking" : false , "queued" : 0 , "backgroundWorkers" : 0 , "tools" : [], "queuedMessages" : []]]
if capability { object [ "capabilities" ] = [ "attachments" : try JSONSerialization . jsonObject ( with : JSONEncoder (). encode ( attachmentLimits )), "future" : true ] }
2026-10-20 14:23:58 -07:00
if assistantImages {
var capabilities = object [ "capabilities" ] as ? [ String : Any ] ?? [:]
capabilities [ "assistantImages" ] = [ "version" : 1 ]
object [ "capabilities" ] = capabilities
}
2026-10-14 19:51:27 -07:00
object [ "unknown" ] = [ "additive" : true ]
return try JSONSerialization . data ( withJSONObject : object )
}
private final class AttachmentTestStore : @ unchecked Sendable {
private let lock = NSLock ()
private var rows : [ AgentOutboxRow ] = []
var storage : AgentOutboxStorage { . init ( read : { self . read () }, write : { value in self . lock . withLock { self . rows = value } }) }
func read () -> [ AgentOutboxRow ] { lock . withLock { rows } }
}
private actor AttachmentTestClient : AgentAPIClienting {
var uploads : [ String ] = []
var submissions : [ AgentSubmission ] = []
var failUploads = 0
var badReceipt = false
var uploadDelay = Duration . milliseconds ( 20 )
var holdDownloads = false
var downloadWaiters : [ String : CheckedContinuation < Data , Never >] = [:]
var downloadStarts : [ String ] = []
var downloadActive = 0
var downloadPeak = 0
var beforeSubmit : (@ Sendable () -> Bool )?
var unavailableDescriptor = false
var unavailableSocket = false
func agentDescriptor () throws -> AgentDescriptor {
if unavailableDescriptor { throw AgentAttachmentIssue ( message : "Synthetic open proxy requires authentication" , code : "attachments_unavailable" , statusCode : 503 ) }
return try AgentCoding . decoder (). decode ( AgentDescriptor . self , from : descriptorData ())
}
func agentMessages ( after : String ?, before : String ?) -> AgentPage { . init ( conversationId : "root" , messages : [], cursor : after ?? "0" , hasMore : false ) }
func agentReadState () -> AgentReadState { . init ( cursor : "0" ) }
func agentMarkRead ( cursor : String ) -> AgentReadState { . init ( cursor : cursor ) }
func agentSubmit ( content : String , requestID : String ) -> AgentReceipt { . init ( id : 1 , status : "queued" , messageId : "inbound:sybil: \( requestID ) " ) }
func agentSubmit ( _ submission : AgentSubmission ) throws -> AgentReceipt {
# expect ( beforeSubmit ?() ?? true )
submissions . append ( submission )
return . init ( id : 1 , status : "queued" , messageId : "inbound:sybil: \( submission . requestId ) " , attachmentIds : badReceipt ? nil : submission . attachmentIDs )
}
func agentUpload ( file : AgentPreparedAttachment , uploadID : String ) async throws -> AgentAttachment {
uploads . append ( uploadID )
try await Task . sleep ( for : uploadDelay )
if failUploads > 0 { failUploads -= 1 ; throw AgentAttachmentIssue ( message : "Free storage or retry later." , code : "attachment_quota_exceeded" , statusCode : 507 , details : [ "futureQuotaField" : . number ( 1 )]) }
return AgentAttachment ( id : "server- \( uploadID ) " , kind : . file , filename : file . filename , mimeType : file . mimeType , sizeBytes : file . data . count , sha256 : file . sha256 )
}
func agentDownload ( _ attachment : AgentAttachment ) async -> Data {
downloadStarts . append ( attachment . id ); downloadActive += 1 ; downloadPeak = max ( downloadPeak , downloadActive )
defer { downloadActive -= 1 }
if holdDownloads { return await withCheckedContinuation { downloadWaiters [ attachment . id ] = $0 } }
return originalBytes
}
func pauseDownloads () { holdDownloads = true }
func releaseDownload ( _ id : String ) { downloadWaiters . removeValue ( forKey : id )?. resume ( returning : originalBytes ) }
func downloadCounts () -> ( Int , Int ) { ( downloadStarts . count , downloadPeak ) }
func downloadedIDs () -> [ String ] { downloadStarts }
func agentSocket ( after : String ) throws -> any AgentSocket {
if unavailableSocket { return AttachmentUnavailableSocket () }
throw AgentFailure ( message : "No socket in unit fixture" )
}
func blockDescriptor () { unavailableDescriptor = true }
func blockSocket () { unavailableSocket = true }
func failNextUpload ( delay : Duration = . milliseconds ( 20 )) { failUploads = 1 ; uploadDelay = delay }
func delayUploads ( _ delay : Duration ) { uploadDelay = delay }
func malformedReceipts ( _ value : Bool ) { badReceipt = value }
func requirePersisted ( _ check : @ escaping @ Sendable () -> Bool ) { beforeSubmit = check }
func uploadIDs () -> [ String ] { uploads }
func sent () -> [ AgentSubmission ] { submissions }
}
private struct AttachmentUnavailableSocket : AgentSocket {
func receive () throws -> Data { throw AgentFailure ( message : "Synthetic closed socket" ) }
func send ( _ text : String ) {}
func close () {}
func closeCode () -> Int { 4503 }
}
@ MainActor private func attachmentEventually ( _ predicate : () -> Bool ) async -> Bool {
for _ in 0. .< 200 { if predicate () { return true }; try ? await Task . sleep ( for : . milliseconds ( 10 )) }
return false
}
private final class AttachmentURLProtocol : URLProtocol , @ unchecked Sendable {
private static let lock = NSLock ()
nonisolated ( unsafe ) private static var handler : (@ Sendable ( URLRequest ) throws -> ( Int , [ String : String ], Data ))?
static func install ( _ value : @ escaping @ Sendable ( URLRequest ) throws -> ( Int , [ String : String ], Data )) { lock . withLock { handler = value } }
override class func canInit ( with request : URLRequest ) -> Bool { true }
override class func canonicalRequest ( for request : URLRequest ) -> URLRequest { request }
override func startLoading () {
do {
let handler = Self . lock . withLock { Self . handler ! }
let ( status , headers , data ) = try handler ( request )
client ?. urlProtocol ( self , didReceive : HTTPURLResponse ( url : request . url !, statusCode : status , httpVersion : nil , headerFields : headers ) ! , cacheStoragePolicy : . notAllowed )
client ?. urlProtocol ( self , didLoad : data )
client ?. urlProtocolDidFinishLoading ( self )
} catch { client ?. urlProtocol ( self , didFailWithError : error ) }
}
override func stopLoading () {}
}
private func attachmentSession () -> URLSession {
let config = URLSessionConfiguration . ephemeral ; config . protocolClasses = [ AttachmentURLProtocol . self ]
return URLSession ( configuration : config )
}
private func requestBytes ( _ request : URLRequest ) -> Data {
if let body = request . httpBody { return body }
guard let stream = request . httpBodyStream else { return Data () }
stream . open (); defer { stream . close () }
var data = Data (), buffer = [ UInt8 ]( repeating : 0 , count : 4096 )
while stream . hasBytesAvailable { let count = stream . read (& buffer , maxLength : buffer . count ); if count <= 0 { break }; data . append ( contentsOf : buffer . prefix ( count )) }
return data
}
@ Suite (. serialized ) struct AgentAttachmentTests {
2026-10-20 14:23:58 -07:00
@ Test @ MainActor func assistantCapabilityDoesNotAdmitInputAndRequiresToken () async throws {
AttachmentURLProtocol . install { request in
# expect ( request . url !. path == "/api/v1/agent" ) // Never upload or submit references.
return ( 200 , [:], try descriptorData ( capability : false , assistantImages : true ))
}
let session = attachmentSession (); defer { session . invalidateAndCancel () }
for token in [ nil , " " , "synthetic-test" ] {
let client = SybilAPIClient ( configuration : . init ( baseURL : URL ( string : "https://fixture.invalid/api" ) ! , authToken : token ), session : session )
let descriptor = try await client . agentDescriptor ()
# expect ( descriptor . capabilities ?. attachments == nil )
# expect (( descriptor . capabilities ?. assistantImages ?. supported == true ) == ( token == "synthetic-test" ))
let draft = AgentAttachmentDraft ()
draft . configure ( client : client , storage : nil ); draft . updateCapability ( descriptor . capabilities ?. attachments )
# expect ( ! draft . supportsAttachments )
# expect ( draft . add ( filename : "original.bin" ) { . init ( filename : "original.bin" , mimeType : "application/octet-stream" , data : originalBytes ) } == nil )
await # expect ( throws : AgentAttachmentIssue . self ) { try await client . agentUpload ( file : . init ( filename : "original.bin" , mimeType : "application/octet-stream" , data : originalBytes ), uploadID : UUID (). uuidString ) }
await # expect ( throws : AgentAttachmentIssue . self ) { try await client . agentSubmit ( AgentSubmission ( content : "" , requestID : "output-only" , attachments : [ sampleAttachment ()])) }
}
for json in [# "{" assistantImages ":{" version ":2}}" #, # "{" assistantImages ":{" version ":" future "}}" #] {
# expect ( try JSONDecoder (). decode ( AgentCapabilities . self , from : Data ( json . utf8 )). assistantImages ?. supported != true )
}
}
@ Test func assistantDescriptorsRequireCompletedAssistantImageAndSafeFields () throws {
let image = AgentAttachment ( id : "output-image" , kind : . image , filename : "landscape.png" , mimeType : "image/png" , sizeBytes : 12 , sha256 : String ( repeating : "a" , count : 64 ))
var row = Message ( id : "outbound:1" , createdAt : Date (), role : . assistant , content : "" , metadata : . object ([
"kind" : . string ( "message" ), "status" : . string ( "complete" ), "agentAttachments" : . array ([ image . metadataValue ])]))
# expect ( row . agentAttachments == [ image ])
for role in [ MessageRole . system , . tool ] {
var wrong = row ; wrong . role = role
# expect ( wrong . agentAttachments . isEmpty )
}
for ( key , value ) in [( "kind" , "tool_call" ), ( "kind" , "other" ), ( "status" , "queued" ), ( "status" , "streaming" )] {
var wrong = row , fields = row . metadata !. objectValue !
fields [ key ] = . string ( value ); wrong . metadata = . object ( fields )
# expect ( wrong . agentAttachments . isEmpty )
}
var descriptors : [ JSONValue ] = [ sampleAttachment (). metadataValue ]
for key in [ "workspacePath" , "sourcePath" , "privatePath" , "url" , "sourceURL" , "downloadUrl" , "external" , "future" ] {
var fields = image . metadataValue . objectValue !; fields [ key ] = . string ( "/private/synthetic.png" )
descriptors . append (. object ( fields ))
}
for ( key , value ) in [( "filename" , "../private.png" ), ( "filename" , "https://example.invalid/a.png" ), ( "mimeType" , "image/svg+xml" ), ( "sha256" , "bad" )] {
var fields = image . metadataValue . objectValue !; fields [ key ] = . string ( value )
descriptors . append (. object ( fields ))
}
for descriptor in descriptors {
row . metadata = . object ([ "kind" : . string ( "message" ), "status" : . string ( "complete" ), "agentAttachments" : . array ([ descriptor ])])
# expect ( row . agentAttachments . isEmpty )
2026-10-20 14:28:59 -07:00
row . metadata = . object ([ "kind" : . string ( "message" ), "status" : . string ( "complete" ), "agentAttachments" : . array ([ image . metadataValue , descriptor ])])
# expect ( row . agentAttachments . isEmpty ) // No valid prefix from an invalid output set.
}
let maximum = ( 0. .< 8 ). map { index in
var item = image ; item . id = "image- \( index ) " ; item . sizeBytes = 4 * 1024 * 1024
return item
}
row . metadata = . object ([ "kind" : . string ( "message" ), "status" : . string ( "complete" ), "agentAttachments" : . array ( maximum . map ( \ . metadataValue ))])
# expect ( row . agentAttachments == maximum ) // Exactly eight IDs and 32 MiB.
let tooMany = ( 0. .< 9 ). map { index in
var item = image ; item . id = "image- \( index ) "
return item
}
var tooLarge = maximum ; tooLarge [ 0 ]. sizeBytes += 1
for invalid in [ tooMany , [ image , image ], tooLarge ] {
row . role = . assistant
row . metadata = . object ([ "kind" : . string ( "message" ), "status" : . string ( "complete" ), "agentAttachments" : . array ( invalid . map ( \ . metadataValue ))])
# expect ( row . agentAttachments . isEmpty )
row . role = . user
# expect ( row . agentAttachments == invalid ) // These new output limits do not change legacy input parsing.
2026-10-20 14:23:58 -07:00
}
row . role = . user
row . metadata = . object ([ "agentAttachments" : . array ([ sampleAttachment (). metadataValue ])])
# expect ( row . agentAttachments == [ sampleAttachment ()])
}
@ Test @ MainActor func imageOnlyAssistantLiveAndHistoryUseSameMergeAndAuthenticatedLoader () async throws {
let png = try # require ( UIGraphicsImageRenderer ( size : CGSize ( width : 24 , height : 16 )). image { context in
UIColor . systemTeal . setFill (); context . fill ( CGRect ( x : 0 , y : 0 , width : 24 , height : 16 ))
}. pngData ())
let attachment = AgentAttachment ( id : "output/opaque" , kind : . image , filename : "landscape.png" , mimeType : "image/png" , sizeBytes : png . count ,
sha256 : SHA256 . hash ( data : png ). map { String ( format : "%02x" , $0 ) }. joined ())
let row = Message ( id : "outbound:1" , createdAt : Date ( timeIntervalSince1970 : 1 ), role : . assistant , content : "" , metadata : . object ([
"kind" : . string ( "message" ), "status" : . string ( "complete" ), "entryId" : . number ( 1 ), "agentAttachments" : . array ([ attachment . metadataValue ])]))
let page = AgentPage ( conversationId : "fixture-root" , messages : [ row ], cursor : "1" , hasMore : false )
let encoder = JSONEncoder (); encoder . dateEncodingStrategy = . iso8601
let bytes = try encoder . encode ( page )
var envelope = try JSONSerialization . jsonObject ( with : bytes ) as ! [ String : Any ]; envelope [ "type" ] = "messages"
let decodedEvent = try AgentEvent . decode ( JSONSerialization . data ( withJSONObject : envelope ))
let event = try # require ( decodedEvent )
let room = SybilAgentRoom (); defer { room . stop () }
try room . receive ( event ); room . flush ()
var history = AgentRoomState ()
try history . apply ( AgentCoding . decoder (). decode ( AgentPage . self , from : bytes ))
// Replay must not duplicate a completed output; no current capability is needed.
try room . receive ( event ); room . flush ()
# expect ( AgentTranscript ( messages : room . messages ). displayed == AgentTranscript ( messages : history . messages ). displayed )
# expect ( room . messages . count == 1 ); # expect ( room . messages [ 0 ]. content . isEmpty )
# expect ( room . messages [ 0 ]. agentAttachments == [ attachment ])
var presentation = AgentPresentation ()
presentation . receive ( event , before : AgentRoomState (), after : history , replay : false , now : Date ())
# expect ( presentation . suppressed )
AttachmentURLProtocol . install { request in
# expect ( request . url !. absoluteString == "https://fixture.invalid/api/v1/agent/attachments/output%2Fopaque/content" )
# expect ( request . httpMethod == "GET" )
# expect ( request . value ( forHTTPHeaderField : "Authorization" ) == "Bearer synthetic-test" )
return ( 200 , [ "Content-Length" : String ( png . count )], png )
}
let session = attachmentSession (); defer { session . invalidateAndCancel () }
let client = SybilAPIClient ( configuration : . init ( baseURL : URL ( string : "https://fixture.invalid/api" ) ! , authToken : "synthetic-test" ), session : session )
let downloads = AgentAttachmentDownloads ()
# expect ( try await downloads . data ( for : attachment , client : client ) == png )
AttachmentURLProtocol . install { _ in Issue . record ( "Verified output should be cached" ); return ( 500 , [:], Data ()) }
# expect ( try await downloads . data ( for : attachment , client : client ) == png )
AttachmentURLProtocol . install { _ in ( 200 , [:], Data ( repeating : 0 , count : png . count )) }
await # expect ( throws : AgentAttachmentIssue . self ) { try await client . agentDownload ( attachment ) }
let unauthenticated = SybilAPIClient ( configuration : . init ( baseURL : URL ( string : "https://fixture.invalid/api" ) ! ), session : session )
await # expect ( throws : AgentAttachmentIssue . self ) { try await unauthenticated . agentDownload ( attachment ) }
}
2026-10-14 19:51:27 -07:00
@ Test func additiveCapabilityAndDistinctMetadata () throws {
let old = try AgentCoding . decoder (). decode ( AgentDescriptor . self , from : descriptorData ( capability : false ))
# expect ( old . capabilities == nil )
let current = try AgentCoding . decoder (). decode ( AgentDescriptor . self , from : descriptorData ())
# expect ( current . capabilities ?. attachments ?. supported == true )
var future = try JSONSerialization . jsonObject ( with : descriptorData ()) as ! [ String : Any ]
future [ "capabilities" ] = [ "attachments" : [ "version" : 9 , "newWire" : true ]]
let unsupported = try AgentCoding . decoder (). decode ( AgentDescriptor . self , from : JSONSerialization . data ( withJSONObject : future ))
# expect ( unsupported . capabilities ?. attachments == nil )
var other = attachmentLimits ; other . version = 2
# expect ( ! other . supported )
let attachment = sampleAttachment ()
let message = Message ( id : "user" , createdAt : Date (), role : . user , content : "caption" , metadata : . object ([ "agentAttachments" : . array ([ attachment . metadataValue ]), "future" : . bool ( true )]))
# expect ( message . attachments . isEmpty )
# expect ( message . agentAttachments == [ attachment ])
# expect ( message . content == "caption" )
let legacy = Data (# "{" id ":" x "," kind ":" file "," filename ":" x . bin "," mimeType ":" application / octet - stream "," sizeBytes ":0," sha256 ":" e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 "," unknown ":true}" #. utf8 )
# expect ( try JSONDecoder (). decode ( AgentAttachment . self , from : legacy ). workspacePath == nil )
}
@ Test func safeNamesAndExactOriginalBytes () throws {
# expect ( try AgentAttachmentPreparation . filename ( "cafe\u{301}.txt" ) == "café.txt" )
for invalid in [ "" , "." , ".." , "../secret" , "x/y" , "x \\ y" , "a\u{0000}b" , "x \n y" , String ( repeating : "é" , count : 128 )] {
# expect ( throws : AgentAttachmentIssue . self ) { try AgentAttachmentPreparation . filename ( invalid ) }
}
let file = try AgentAttachmentPreparation . prepare ( originalBytes , filename : "archive.zip" , mimeType : "application/zip" , limits : attachmentLimits )
# expect ( file . data == originalBytes )
let text = try AgentAttachmentPreparation . prepare ( originalBytes , filename : "invalid-utf8.txt" , mimeType : "text/plain" , limits : attachmentLimits )
# expect ( text . data == originalBytes ) // Server classifies as file; client never decodes/truncates upload bytes.
# expect ( try AgentAttachmentPreparation . prepare ( Data (), filename : "empty.txt" , mimeType : "text/plain" , limits : attachmentLimits ). data . isEmpty )
var small = attachmentLimits ; small . maxFileBytes = 2
# expect ( throws : AgentAttachmentIssue . self ) { try AgentAttachmentPreparation . prepare ( originalBytes , filename : "x.bin" , mimeType : "application/octet-stream" , limits : small ) }
}
@ Test @ MainActor func rasterValidationAndFileSelection () throws {
let image = UIGraphicsImageRenderer ( size : CGSize ( width : 12 , height : 8 )). image { context in UIColor . red . setFill (); context . fill ( CGRect ( x : 0 , y : 0 , width : 12 , height : 8 )) }
let png = try # require ( image . pngData ())
# expect ( try AgentAttachmentPreparation . prepare ( png , filename : "red.png" , mimeType : "image/png" , limits : attachmentLimits ). data == png )
# expect ( throws : AgentAttachmentIssue . self ) { try AgentAttachmentPreparation . raster ( png , mime : "image/jpeg" ) }
# expect ( throws : AgentAttachmentIssue . self ) { try AgentAttachmentPreparation . raster ( png , mime : "image/png" , pixels : 20 ) }
# expect ( throws : AgentAttachmentIssue . self ) { try AgentAttachmentPreparation . raster ( Data ( "<svg/>" . utf8 ), mime : "image/svg+xml" ) }
let directory = FileManager . default . temporaryDirectory . appendingPathComponent ( UUID (). uuidString )
try FileManager . default . createDirectory ( at : directory , withIntermediateDirectories : true )
defer { try ? FileManager . default . removeItem ( at : directory ) }
let url = directory . appendingPathComponent ( "sample.pdf" ); try originalBytes . write ( to : url )
let file = try AgentAttachmentPreparation . file ( url , limits : attachmentLimits )
# expect ( file . data == originalBytes ); # expect ( file . mimeType == "application/pdf" )
}
@ Test @ MainActor func syntheticHEICPhotoConvertsButFileKeepsOriginal () async throws {
let image = UIGraphicsImageRenderer ( size : CGSize ( width : 2400 , height : 1800 )). image { context in
UIColor . blue . setFill (); context . fill ( CGRect ( x : 0 , y : 0 , width : 2400 , height : 1800 ))
}
let encoded = NSMutableData ()
let destination = try # require ( CGImageDestinationCreateWithData ( encoded , UTType . heic . identifier as CFString , 1 , nil ), "Simulator must provide an ImageIO HEIC encoder for this regression" )
CGImageDestinationAddImage ( destination , try # require ( image . cgImage ), nil )
# expect ( CGImageDestinationFinalize ( destination ), "Synthetic HEIC encoding must succeed" )
let data = encoded as Data
let prepared = try await AgentAttachmentPreparation . offMain {
try AgentAttachmentPreparation . photo ( data , filename : "cafe\u{301}.HEIC" , mimeType : "image/heic" , limits : attachmentLimits )
}
# expect ( prepared . filename == "café.jpg" ); # expect ( prepared . mimeType == "image/jpeg" )
# expect ( prepared . data . count <= attachmentLimits . imageLimit )
let source = try # require ( CGImageSourceCreateWithData ( prepared . data as CFData , nil ))
# expect ( CGImageSourceGetType ( source ) as String ? == UTType . jpeg . identifier )
let properties = try # require ( CGImageSourceCopyPropertiesAtIndex ( source , 0 , nil ) as ? [ CFString : Any ])
# expect (( properties [ kCGImagePropertyPixelWidth ] as ? Int ?? Int . max ) <= 2000 )
# expect (( properties [ kCGImagePropertyPixelHeight ] as ? Int ?? Int . max ) <= 2000 )
let root = FileManager . default . temporaryDirectory . appendingPathComponent ( UUID (). uuidString )
try FileManager . default . createDirectory ( at : root , withIntermediateDirectories : true )
defer { try ? FileManager . default . removeItem ( at : root ) }
let url = root . appendingPathComponent ( "original.heic" ); try data . write ( to : url )
let file = try AgentAttachmentPreparation . file ( url , limits : attachmentLimits )
# expect ( file . data == data ); # expect ( file . filename == "original.heic" ); # expect ( file . mimeType == "image/heic" )
var tooSmall = attachmentLimits ; tooSmall . maxImagePixels = 100
# expect ( throws : AgentAttachmentIssue . self ) { try AgentAttachmentPreparation . photo ( data , filename : "large.heic" , mimeType : "image/heic" , limits : tooSmall ) }
}
@ Test @ MainActor func downloadCoordinatorBoundsAndCancelsQueuedTransfers () async throws {
let client = AttachmentTestClient (), downloads = AgentAttachmentDownloads ()
await client . pauseDownloads ()
let one = Task { try await downloads . data ( for : sampleAttachment ( "one" ), client : client ) }
let two = Task { try await downloads . data ( for : sampleAttachment ( "two" ), client : client ) }
for _ in 0. .< 100 { if await client . downloadCounts (). 0 == 2 { break }; try await Task . sleep ( for : . milliseconds ( 10 )) }
let three = Task { try await downloads . data ( for : sampleAttachment ( "three" ), client : client ) }
let cancelled = Task { try await downloads . data ( for : sampleAttachment ( "cancelled" ), client : client ) }
try await Task . sleep ( for : . milliseconds ( 30 )); cancelled . cancel ()
await # expect ( throws : CancellationError . self ) { try await cancelled . value }
# expect ( await client . downloadCounts (). 0 == 2 )
await client . releaseDownload ( "one" ); _ = try await one . value
for _ in 0. .< 100 { if await client . downloadCounts (). 0 == 3 { break }; try await Task . sleep ( for : . milliseconds ( 10 )) }
# expect ( await client . downloadCounts (). 1 == 2 )
# expect ( await client . downloadedIDs (). contains ( "cancelled" ) == false )
await client . releaseDownload ( "two" ); await client . releaseDownload ( "three" )
_ = try await two . value ; _ = try await three . value
# expect ( try await downloads . data ( for : sampleAttachment ( "one" ), client : client ) == originalBytes )
# expect ( await client . downloadCounts (). 0 == 3 ) // Verified cache hit.
}
@ Test @ MainActor func configurationResetRejectsOldDownloadsAndCache () async throws {
let old = AttachmentTestClient (), next = AttachmentTestClient (), room = SybilAgentRoom ()
room . configure ( client : old , storage : AttachmentTestStore (). storage )
defer { room . stop () }
await old . pauseDownloads ()
let stale = Task { try await room . downloadAttachment ( sampleAttachment ()) }
for _ in 0. .< 100 { if await old . downloadCounts (). 0 == 1 { break }; try await Task . sleep ( for : . milliseconds ( 10 )) }
room . configure ( client : next , storage : AttachmentTestStore (). storage )
await old . releaseDownload ( sampleAttachment (). id )
await # expect ( throws : CancellationError . self ) { try await stale . value }
# expect ( try await room . downloadAttachment ( sampleAttachment ()) == originalBytes )
# expect ( await next . downloadCounts (). 0 == 1 )
# expect ( try await room . downloadAttachment ( sampleAttachment ()) == originalBytes )
# expect ( await next . downloadCounts (). 0 == 1 )
}
@ Test @ MainActor func invalidatedDownloadQueueNeverStartsOrCaches () async throws {
let client = AttachmentTestClient (), downloads = AgentAttachmentDownloads ()
await client . pauseDownloads ()
let one = Task { try await downloads . data ( for : sampleAttachment ( "one" ), client : client ) }
let two = Task { try await downloads . data ( for : sampleAttachment ( "two" ), client : client ) }
for _ in 0. .< 100 { if await client . downloadCounts (). 0 == 2 { break }; try await Task . sleep ( for : . milliseconds ( 10 )) }
let queued = Task { try await downloads . data ( for : sampleAttachment ( "queued" ), client : client ) }
try await Task . sleep ( for : . milliseconds ( 20 ))
await downloads . invalidate ()
await # expect ( throws : CancellationError . self ) { try await queued . value }
await client . releaseDownload ( "one" ); await client . releaseDownload ( "two" )
await # expect ( throws : CancellationError . self ) { try await one . value }
await # expect ( throws : CancellationError . self ) { try await two . value }
await # expect ( throws : CancellationError . self ) { try await downloads . data ( for : sampleAttachment ( "one" ), client : client ) }
# expect ( await client . downloadCounts (). 0 == 2 )
}
@ Test func orderedReferenceAndReceiptValidation () throws {
let a = sampleAttachment ( "one" ), b = sampleAttachment ( "two" )
let request = try AgentSubmission ( content : " " , requestID : "stable" , attachments : [ a , b ])
let fields = try # require ( JSONSerialization . jsonObject ( with : JSONEncoder (). encode ( request )) as ? [ String : Any ])
# expect (( fields [ "attachments" ] as ? [[ String : String ]]) == [[ "id" : "one" ], [ "id" : "two" ]])
# expect ( throws : AgentAttachmentIssue . self ) { try AgentSubmission ( content : "" , requestID : "stable" ) }
# expect ( throws : AgentAttachmentIssue . self ) { try AgentSubmission ( content : "" , requestID : "stable" , attachments : [ a , a ]) }
for ids : [ String ]? in [ nil , [], [ "one" ], [ "two" , "one" ], [ "one" , "two" , "extra" ]] {
let receipt = AgentReceipt ( id : 1 , status : "queued" , messageId : "inbound:sybil:stable" , attachmentIds : ids )
# expect ( throws : AgentAttachmentIssue . self ) { try receipt . validate ( requestID : "stable" , attachmentIDs : [ "one" , "two" ]) }
}
try AgentReceipt ( id : 1 , status : "queued" , messageId : "inbound:sybil:stable" , attachmentIds : [ "one" , "two" ]). validate ( requestID : "stable" , attachmentIDs : request . attachmentIDs )
}
@ Test func multipartPreservesBinaryAndBoundary () throws {
let id = UUID (). uuidString
let file = AgentPreparedAttachment ( filename : "café \" original \" .bin" , mimeType : "application/octet-stream" , data : originalBytes )
let multipart = try AgentMultipart ( file : file , uploadID : id )
let prefix = Data ( " \r\n\r\n " . utf8 )
# expect ( multipart . body . range ( of : originalBytes ) != nil )
let printable = String ( decoding : multipart . body , as : UTF8 . self )
# expect ( printable . components ( separatedBy : "Content-Disposition:" ). count == 3 )
# expect ( printable . contains ( "name= \" uploadId \"\r\n\r\n \( id ) " ))
# expect ( printable . contains ( "filename= \" café \\\" original \\\" .bin \" " ))
# expect ( multipart . body . suffix ( Data ( " \r\n -- \( multipart . boundary ) -- \r\n " . utf8 ). count ) == Data ( " \r\n -- \( multipart . boundary ) -- \r\n " . utf8 ))
# expect ( multipart . body . range ( of : prefix ) != nil )
}
@ Test func uploadTransportAuthBytesAndReceipt () async throws {
let file = AgentPreparedAttachment ( filename : "original.bin" , mimeType : "application/octet-stream" , data : originalBytes ), id = UUID (). uuidString
AttachmentURLProtocol . install { request in
# expect ( request . value ( forHTTPHeaderField : "Authorization" ) == "Bearer synthetic-test" )
# expect ( request . url ?. query == nil )
if request . url !. path == "/api/v1/agent" { return ( 200 , [:], try descriptorData ()) }
# expect ( request . url !. path == "/api/v1/agent/attachments" ); # expect ( request . httpMethod == "POST" )
let contentType = try # require ( request . value ( forHTTPHeaderField : "Content-Type" ))
let boundary = try # require ( contentType . components ( separatedBy : "boundary=" ). last )
let bytes = requestBytes ( request )
# expect ( bytes . starts ( with : Data ( "-- \( boundary ) \r\n " . utf8 )))
# expect ( bytes . range ( of : originalBytes ) != nil ); # expect ( bytes . range ( of : Data ( id . utf8 )) != nil )
return ( 201 , [:], try JSONEncoder (). encode ([ "attachment" : sampleAttachment ()]))
}
let session = attachmentSession (); defer { session . invalidateAndCancel () }
let client = SybilAPIClient ( configuration : . init ( baseURL : URL ( string : "https://fixture.invalid/api" ) ! , authToken : "synthetic-test" ), session : session )
# expect ( try await client . agentUpload ( file : file , uploadID : id ) == sampleAttachment ())
}
@ Test func missingCapabilityNeverUploadsOrSubmitsReferences () async throws {
AttachmentURLProtocol . install { request in
# expect ( request . url !. path == "/api/v1/agent" )
return ( 200 , [:], try descriptorData ( capability : false ))
}
let session = attachmentSession (); defer { session . invalidateAndCancel () }
let client = SybilAPIClient ( configuration : . init ( baseURL : URL ( string : "https://fixture.invalid/api" ) ! ), session : session )
await # expect ( throws : AgentAttachmentIssue . self ) { try await client . agentUpload ( file : . init ( filename : "original.bin" , mimeType : "application/octet-stream" , data : originalBytes ), uploadID : UUID (). uuidString ) }
await # expect ( throws : AgentAttachmentIssue . self ) { try await client . agentSubmit ( AgentSubmission ( content : "" , requestID : "stable" , attachments : [ sampleAttachment ()])) }
}
@ Test func openPublicAPIDisablesAttachmentCapabilityAndDownloads () async throws {
AttachmentURLProtocol . install { request in
# expect ( request . url !. path == "/v1/agent" )
return ( 200 , [:], try descriptorData ())
}
let session = attachmentSession (); defer { session . invalidateAndCancel () }
let client = SybilAPIClient ( configuration : . init ( baseURL : URL ( string : "https://fixture.invalid" ) ! ), session : session )
# expect ( try await client . agentDescriptor (). capabilities ?. attachments == nil )
await # expect ( throws : AgentAttachmentIssue . self ) { try await client . agentDownload ( sampleAttachment ()) }
}
@ Test func quotaCapabilitySnapshotAndDotFilenameAreAdditive () throws {
var fields = try JSONSerialization . jsonObject ( with : JSONEncoder (). encode ( attachmentLimits )) as ! [ String : Any ]
fields . merge ([ "quotaBytes" : 1073741824 , "usedBytes" : 20 , "remainingBytes" : 1073741804 , "maxConcurrentUploads" : 2 , "maxConcurrentDecodes" : 1 , "future" : true ]) { _ , next in next }
let limits = try JSONDecoder (). decode ( AgentAttachmentCapabilities . self , from : JSONSerialization . data ( withJSONObject : fields ))
# expect ( limits . supported ); # expect ( limits . remainingBytes == 1073741804 )
# expect ( limits . maxConcurrentUploads == 2 ); # expect ( limits . maxConcurrentDecodes == 1 )
var attachment = sampleAttachment ( name : ".notes.txt" )
attachment . workspacePath = "/data/workspace/uploads/server-id/_notes.txt"
try attachment . validate ()
# expect ( attachment . filename == ".notes.txt" )
# expect ( attachment . workspacePath ?. hasSuffix ( "/_notes.txt" ) == true )
}
@ Test func downloadAuthOpaqueIDExactBytesAndLimits () async throws {
let attachment = sampleAttachment ( "opaque/part?value#x" )
AttachmentURLProtocol . install { request in
# expect ( request . url !. absoluteString == "https://fixture.invalid/api/v1/agent/attachments/opaque%2Fpart%3Fvalue%23x/content" )
# expect ( request . value ( forHTTPHeaderField : "Authorization" ) == "Bearer synthetic-test" )
# expect ( request . cachePolicy == . reloadIgnoringLocalCacheData )
return ( 200 , [ "Content-Length" : String ( originalBytes . count )], originalBytes )
}
let session = attachmentSession (); defer { session . invalidateAndCancel () }
let client = SybilAPIClient ( configuration : . init ( baseURL : URL ( string : "https://fixture.invalid/api/" ) ! , authToken : "synthetic-test" ), session : session )
# expect ( try await client . agentDownload ( attachment ) == originalBytes )
AttachmentURLProtocol . install { _ in ( 200 , [:], originalBytes + Data ([ 9 ])) }
await # expect ( throws : AgentAttachmentIssue . self ) { try await client . agentDownload ( attachment ) }
AttachmentURLProtocol . install { _ in ( 200 , [:], Data ( repeating : 1 , count : originalBytes . count )) }
await # expect ( throws : AgentAttachmentIssue . self ) { try await client . agentDownload ( attachment ) }
}
@ Test func errorCodesAndAdditiveQuotaFieldsSurvive () async throws {
for status in [ 409 , 413 , 422 , 429 , 507 , 503 ] {
AttachmentURLProtocol . install { request in
if request . url !. path == "/api/v1/agent" { return ( 200 , [:], try descriptorData ()) }
return ( status , status == 429 ? [ "Retry-After" : "2" ] : [:], try JSONSerialization . data ( withJSONObject : [ "error" : true , "message" : "Actionable storage message" , "statusCode" : status , "code" : status == 507 ? "attachment_quota_exceeded" : status == 422 ? "image_model_unsupported" : "attachments_busy" , "futureByteField" : 123 ]))
}
let session = attachmentSession (); defer { session . invalidateAndCancel () }
let client = SybilAPIClient ( configuration : . init ( baseURL : URL ( string : "https://fixture.invalid" ) ! , authToken : "synthetic-test" ), session : session )
do { _ = try await client . agentDownload ( sampleAttachment ()); Issue . record ( "Expected failure" ) }
catch let issue as AgentAttachmentIssue {
# expect ( issue . statusCode == status ); # expect ( issue . message == "Actionable storage message" )
# expect ( issue . details [ "futureByteField" ] == . number ( 123 ))
if status == 507 { # expect ( issue . code == "attachment_quota_exceeded" ) }
if status == 429 { # expect ( issue . retryAfter == "2" ) }
if status == 422 { # expect ( issue . code == "image_model_unsupported" ) }
}
}
}
@ Test func redirectPolicyRejectsUntrustedOrigin () async throws {
let session = attachmentSession (); defer { session . invalidateAndCancel () }
let task = session . dataTask ( with : URL ( string : "https://fixture.invalid/api" ) ! )
let policy = AgentAttachmentRedirectPolicy ()
let response = HTTPURLResponse ( url : URL ( string : "https://fixture.invalid/api" ) ! , statusCode : 302 , httpVersion : nil , headerFields : [:]) !
await withCheckedContinuation { continuation in
policy . urlSession ( session , task : task , willPerformHTTPRedirection : response , newRequest : URLRequest ( url : URL ( string : "https://untrusted.invalid" ) ! )) { request in
# expect ( request == nil ); continuation . resume ()
}
}
task . cancel ()
}
@ Test @ MainActor func orderedSelectionFailureRetryRemoveAndCapability () async throws {
let client = AttachmentTestClient (), draft = AgentAttachmentDraft ()
draft . configure ( client : client , storage : nil ); draft . updateCapability ( attachmentLimits )
await client . failNextUpload ()
let first = try # require ( draft . add ( filename : "one.bin" ) { . init ( filename : "one.bin" , mimeType : "application/octet-stream" , data : originalBytes ) })
let second = try # require ( draft . add ( filename : "two.bin" ) { . init ( filename : "two.bin" , mimeType : "application/octet-stream" , data : originalBytes ) })
# expect ( ! draft . canSend )
# expect ( await attachmentEventually { draft . items [ 0 ]. state == . failed && draft . items [ 1 ]. state == . ready })
# expect ( draft . items [ 0 ]. issue ?. code == "attachment_quota_exceeded" )
# expect ( ! draft . canSend ); draft . retry ( first )
# expect ( await attachmentEventually { draft . items . allSatisfy { $0 . state == . ready } })
# expect ( draft . ready . map ( \ . filename ) == [ "one.bin" , "two.bin" ])
# expect ( await client . uploadIDs () == [ first , second , first ])
# expect ( draft . canSend )
draft . updateCapability ( nil ); # expect ( ! draft . canSend )
# expect ( draft . ready . count == 2 )
draft . remove ( first ); draft . remove ( second ); draft . caption = "Text still works"
# expect ( draft . canSend )
}
@ Test @ MainActor func removedInflightUploadCannotReappear () async throws {
let client = AttachmentTestClient (), draft = AgentAttachmentDraft ()
await client . delayUploads (. milliseconds ( 250 ))
draft . configure ( client : client , storage : nil ); draft . updateCapability ( attachmentLimits )
let id = try # require ( draft . add ( filename : "remove.bin" ) { . init ( filename : "remove.bin" , mimeType : "application/octet-stream" , data : originalBytes ) })
# expect ( await attachmentEventually { draft . items . first ?. state == . uploading })
draft . remove ( id )
try await Task . sleep ( for : . milliseconds ( 300 ))
# expect ( draft . items . isEmpty ); # expect ( ! draft . canSend )
}
@ Test @ MainActor func stagedUploadAndReadyDraftSurviveRelaunch () async throws {
let root = FileManager . default . temporaryDirectory . appendingPathComponent ( UUID (). uuidString )
defer { try ? FileManager . default . removeItem ( at : root ) }
let storage = AgentDraftStorage . file ( baseURL : URL ( string : "https://fixture.invalid/api" ) ! , root : root )
let client = AttachmentTestClient (), first = AgentAttachmentDraft ()
await client . failNextUpload ()
first . configure ( client : client , storage : storage ); first . updateCapability ( attachmentLimits ); first . caption = "Keep my caption"
let id = try # require ( first . add ( filename : "original.bin" ) { . init ( filename : "original.bin" , mimeType : "application/octet-stream" , data : originalBytes ) })
# expect ( await attachmentEventually { first . items . first ?. state == . failed })
first . suspend ()
let next = AgentAttachmentDraft (); next . configure ( client : client , storage : storage ); next . updateCapability ( attachmentLimits )
# expect ( next . caption == "Keep my caption" ); # expect ( next . items . first ?. id == id )
next . retry ( id ); # expect ( await attachmentEventually { next . items . first ?. state == . ready })
# expect ( await client . uploadIDs () == [ id , id ]); # expect ( try storage . bytes ( id ) == originalBytes )
let reopened = AgentAttachmentDraft (); reopened . configure ( client : client , storage : storage )
# expect ( reopened . ready == next . ready ); # expect ( ! reopened . canSend )
reopened . updateCapability ( attachmentLimits ); # expect ( reopened . canSend )
reopened . clearAfterSubmission (); # expect ( try storage . read (). items . isEmpty )
# expect ( throws : ( any Error ). self ) { try storage . bytes ( id ) }
}
@ Test @ MainActor func outboxReferencesPersistBeforePostRetryAndEchoWins () async throws {
let client = AttachmentTestClient (), store = AttachmentTestStore (), room = SybilAgentRoom ()
room . configure ( client : client , storage : store . storage ); defer { room . stop () }
# expect ( await attachmentEventually { ! room . isLoading })
await client . malformedReceipts ( true )
await client . requirePersisted { store . read (). first ?. attachments == [ sampleAttachment ()] }
# expect ( room . send ( "" , attachments : [ sampleAttachment ()]))
# expect ( await attachmentEventually { room . outbox . first ?. status == "failed" })
let original = try # require ( room . outbox . first )
room . retry ( original . id )
# expect ( await attachmentEventually { room . outbox . first ?. status == "failed" })
let submitted = await client . sent ()
# expect ( submitted . count == 2 ); # expect ( submitted . allSatisfy { $0 . requestId == original . requestID && $0 . attachmentIDs == [ sampleAttachment (). id ] })
var echo = original . message
echo . content = "Attached: original.bin."
echo . metadata ?. self = . object ([ "kind" : . string ( "message" ), "entryId" : . number ( 1 ), "status" : . string ( "complete" ), "agentAttachments" : . array ([ sampleAttachment (). metadataValue ])])
try room . receive (. messages (. init ( conversationId : "root" , messages : [ echo ], cursor : "1" , hasMore : false )))
room . flush ()
# expect ( room . outbox . first ?. confirmed == true ); # expect ( store . read (). isEmpty )
# expect ( room . messages . count == 1 ); # expect ( room . messages . first ?. agentAttachments == [ sampleAttachment ()])
# expect ( room . messages . first ?. createdAt == original . createdAt )
}
@ Test @ MainActor func failedDraftHandoffCannotPostOrCreateAnotherIdentityAfterRelaunch () async throws {
let root = FileManager . default . temporaryDirectory . appendingPathComponent ( UUID (). uuidString )
defer { try ? FileManager . default . removeItem ( at : root ) }
let disk = AgentDraftStorage . file ( baseURL : URL ( string : "https://fixture.invalid/api" ) ! , root : root )
let draftID = UUID (). uuidString
let item = AgentDraftItem ( filename : "original.bin" , state : . ready , attachment : sampleAttachment ())
try disk . write (. init ( caption : "Keep caption and references" , items : [ item ], id : draftID ))
var unavailable = disk
unavailable . write = { snapshot in
if snapshot . items . isEmpty { throw AgentAttachmentIssue . local ( "Synthetic disk failure" ) }
try disk . write ( snapshot )
}
let client = AttachmentTestClient (), store = AttachmentTestStore (), room = SybilAgentRoom ()
room . configure ( client : client , storage : store . storage , draftStorage : unavailable )
# expect ( await attachmentEventually { ! room . isLoading })
# expect ( ! room . sendComposer ())
# expect ( room . composer == "Keep caption and references" )
# expect ( room . attachmentDraft . ready == [ sampleAttachment ()])
# expect ( store . read (). count == 1 )
let saved = try # require ( store . read (). first )
# expect ( saved . draftID == draftID )
room . retry ( saved . id )
# expect ( await client . sent (). isEmpty )
room . stop ()
let reopened = SybilAgentRoom ()
reopened . configure ( client : client , storage : store . storage , draftStorage : disk )
defer { reopened . stop () }
# expect ( await attachmentEventually { ! reopened . isLoading })
# expect ( reopened . composer . isEmpty ); # expect ( reopened . attachmentDraft . items . isEmpty )
# expect ( ! reopened . sendComposer ())
reopened . retry ( saved . id )
# expect ( await attachmentEventually { reopened . outbox . first ?. seenOnServer == true })
# expect ( await client . sent (). map ( \ . requestId ) == [ saved . requestID ])
# expect ( await client . sent (). first ?. attachmentIDs == [ sampleAttachment (). id ])
}
@ Test @ MainActor func openProxyHTTP503AndSocket4503KeepReadyDraftAndOutbox () async throws {
for socketFailure in [ false , true ] {
let client = AttachmentTestClient (), store = AttachmentTestStore (), room = SybilAgentRoom ()
if socketFailure { await client . blockSocket () } else { await client . blockDescriptor () }
let row = AgentOutboxRow ( requestID : "unchanged-retry" , content : "Retained caption" , createdAt : Date (), attachments : [ sampleAttachment ()])
try store . storage . write ([ row ])
let snapshot = AgentDraftSnapshot ( caption : "Retained draft" , items : [. init ( filename : "original.bin" , state : . ready , attachment : sampleAttachment ())])
let draft = AgentDraftStorage ( read : { snapshot }, write : { _ in }, stage : { _ , _ in }, bytes : { _ in originalBytes }, remove : { _ in })
room . configure ( client : client , storage : store . storage , draftStorage : draft )
# expect ( await attachmentEventually { room . error ?. contains ( socketFailure ? "4503" : "Synthetic open proxy" ) == true })
# expect ( ! room . attachmentDraft . supportsAttachments )
# expect ( room . composer == "Retained draft" ); # expect ( room . attachmentDraft . ready == [ sampleAttachment ()])
# expect ( ! room . sendComposer ())
# expect ( room . outbox . first ?. requestID == "unchanged-retry" )
# expect ( store . read (). first ?. attachments == [ sampleAttachment ()])
# expect ( await client . sent (). isEmpty )
room . stop ()
}
}
@ Test @ MainActor func mismatchedCanonicalAndQueuedEchoesNeverHideLocalReferencesOrRetry () async throws {
let client = AttachmentTestClient (), store = AttachmentTestStore (), room = SybilAgentRoom ()
await client . malformedReceipts ( true )
room . configure ( client : client , storage : store . storage ); defer { room . stop () }
# expect ( await attachmentEventually { ! room . isLoading })
let attachments = [ sampleAttachment ( "first" ), sampleAttachment ( "second" )]
# expect ( room . send ( "Original caption" , attachments : attachments ))
# expect ( await attachmentEventually { room . outbox . first ?. status == "failed" })
let row = try # require ( room . outbox . first )
for ( index , wrong ) in [[], Array ( attachments . reversed ()), [ attachments [ 0 ]]]. enumerated () {
var message = row . message
message . content = "Incorrect server echo"
message . metadata = . object ([ "kind" : . string ( "message" ), "status" : . string ( "complete" ), "entryId" : . number ( Double ( index + 1 )), "agentAttachments" : . array ( wrong . map ( \ . metadataValue ))])
try room . receive (. messages (. init ( conversationId : "root" , messages : [ message ], cursor : String ( index + 1 ), hasMore : false )))
var activity = room . snapshot . room . activity
activity . queuedMessages = [ message ]
try room . receive (. activity ( "root" , activity )); room . flush ()
# expect ( room . outbox . first ?. confirmed == false ); # expect ( room . outbox . first ?. seenOnServer == false )
# expect ( room . outbox . first ?. status == "failed" ); # expect ( room . outbox . first ?. error != nil )
# expect ( room . messages . first ?. agentAttachments == attachments )
# expect ( room . messages . first ?. content == "Original caption" )
# expect ( store . read (). first ?. requestID == row . requestID )
}
}
@ Test func legacyOutboxAndQueuedHistoryKeepDistinctAttachmentsAndHiddenTools () throws {
let row = AgentOutboxRow ( requestID : "stable" , content : "caption" , createdAt : Date ( timeIntervalSince1970 : 1 ))
# expect ( try JSONDecoder (). decode ( AgentOutboxRow . self , from : JSONEncoder (). encode ( row )). attachments == nil )
var attached = row ; attached . attachments = [ sampleAttachment ()]
let restored = try JSONDecoder (). decode ( AgentOutboxRow . self , from : JSONEncoder (). encode ( attached ))
# expect ( restored . attachments == attached . attachments ); # expect ( restored . requestID == row . requestID )
var queued = restored . message ; queued . metadata = . object ([ "kind" : . string ( "message" ), "status" : . string ( "queued" ), "agentAttachments" : . array ([ sampleAttachment (). metadataValue ])])
var committed = queued ; committed . metadata = . object ([ "kind" : . string ( "message" ), "status" : . string ( "complete" ), "entryId" : . number ( 2 ), "agentAttachments" : . array ([ sampleAttachment (). metadataValue ])])
let tool = Message ( id : "hidden" , createdAt : Date (), role : . tool , content : "raw tool" , metadata : . object ([ "kind" : . string ( "tool_call" ), "entryId" : . number ( 3 )]))
var state = AgentRoomState ()
try state . apply (. init ( conversationId : "root" , messages : [ queued , tool ], cursor : "3" , hasMore : false ))
try state . apply (. init ( conversationId : "root" , messages : [ committed ], cursor : "3" , hasMore : false ))
try state . apply (. init ( conversationId : "root" , messages : [ queued ], cursor : "2" , hasMore : false ))
# expect ( state . messages . count == 2 ); # expect ( state . cursor == "3" ); # expect ( state . unreadCount == 0 )
let transcript = AgentTranscript ( messages : state . messages )
# expect ( transcript . messages . count == 1 ); # expect ( transcript . messages [ 0 ]. agentAttachments == [ sampleAttachment ()])
# expect ( transcript . messages [ 0 ]. attachments . isEmpty )
}
}