@@ -653,6 +653,71 @@ struct ProtocolTests {
653653 try expect ( !serialized. contains ( " secret@ " ) , " diagnostics must redact URL credentials " )
654654 }
655655
656+ static func responsesFitTheProtocolFrame( ) throws {
657+ // 500 events each carrying a 4 KiB message is roughly 2 MB — twice the
658+ // frame. Before the response bound this encoded past the limit and the
659+ // agent saw INVALID_REQUEST for a valid `qa report`.
660+ let store = QADiagnosticStore ( )
661+ let wide = String ( repeating: " d " , count: 4_096 )
662+ for _ in 0 ..< 500 {
663+ store. append ( kind: " console " , level: " error " , message: wide, url: " https://example.com/ \( wide) " )
664+ }
665+ let report = store. report ( )
666+ let encoded = try ProtocolCodec . encodeLine (
667+ CommandResponse . success ( id: " report " , result: report)
668+ )
669+ try expect (
670+ encoded. count <= headlessMaximumMessageBytes,
671+ " a full diagnostic report must fit the protocol frame "
672+ )
673+ guard case . object( let object) = report else { throw TestFailure ( description: " report shape " ) }
674+ try expect ( object [ " truncated " ] == . bool( true ) , " a bounded report should report truncation " )
675+ guard case . object( let summary) ? = object [ " summary " ] ,
676+ case . object( let omitted) ? = object [ " omitted " ] ,
677+ case . array( let events) ? = object [ " events " ] ,
678+ case . array( let issues) ? = object [ " issues " ] else {
679+ throw TestFailure ( description: " report bounds " )
680+ }
681+ try expect ( summary [ " events " ] == . number( 500 ) , " summary counts should describe every event " )
682+ try expect ( ( omitted [ " events " ] ? . numberValue ?? 0 ) > 0 , " omitted events should be counted " )
683+ try expect (
684+ events. count + Int( omitted [ " events " ] ? . numberValue ?? 0 ) == 500 ,
685+ " kept plus omitted events should account for the whole buffer "
686+ )
687+ // Issues carry the same message and URL as the events they describe, so
688+ // a count cap is not a size cap — bounding them by bytes is what keeps
689+ // the report inside the frame.
690+ try expect (
691+ issues. count + Int( omitted [ " issues " ] ? . numberValue ?? 0 )
692+ == Int ( summary [ " issues " ] ? . numberValue ?? 0 ) ,
693+ " kept plus omitted issues should account for every issue "
694+ )
695+ }
696+
697+ static func artifactListingStaysBounded( ) throws {
698+ let root = " /tmp/headless-artifact-bound- \( UUID ( ) . uuidString) "
699+ defer { try ? FileManager . default. removeItem ( atPath: root) }
700+ let store = try ArtifactStore ( environment: [ " HEADLESS_ARTIFACT_DIR " : root] )
701+ for index in 0 ..< 260 {
702+ _ = try store. write ( Data ( " x " . utf8) , requestedName: " bound- \( index) .json " , extension: " json " , prefix: " bound " )
703+ }
704+ guard case . object( let listing) = try store. list ( ) ,
705+ case . array( let artifacts) ? = listing [ " artifacts " ] else {
706+ throw TestFailure ( description: " artifact listing " )
707+ }
708+ try expect ( artifacts. count == 250 , " artifact listing should stay bounded " )
709+ try expect ( listing [ " total " ] == . number( 260 ) , " artifact listing should report the true total " )
710+ try expect ( listing [ " omitted " ] == . number( 10 ) , " artifact listing should report what it left out " )
711+ try expect ( listing [ " truncated " ] == . bool( true ) , " a bounded artifact listing is truncated " )
712+ let encoded = try ProtocolCodec . encodeLine (
713+ CommandResponse . success ( id: " artifacts " , result: try store. list ( ) )
714+ )
715+ try expect (
716+ encoded. count <= headlessMaximumMessageBytes,
717+ " an artifact listing must fit the protocol frame "
718+ )
719+ }
720+
656721 static func diagnosticServices( ) throws {
657722 let store = QADiagnosticStore ( )
658723 store. append ( kind: " console " , level: " warn " , message: " first " )
@@ -785,6 +850,8 @@ struct ProtocolTests {
785850 ( " screenshot series helpers " , screenshotSeriesHelpers) ,
786851 ( " diagnostic summary " , diagnosticSummary) ,
787852 ( " diagnostic bounds and URL redaction " , diagnosticsBoundAndRedacted) ,
853+ ( " responses fit the protocol frame " , responsesFitTheProtocolFrame) ,
854+ ( " artifact listing stays bounded " , artifactListingStaysBounded) ,
788855 ( " diagnostic services " , diagnosticServices) ,
789856 ( " diagnostic CLI " , diagnosticCLI) ,
790857 ( " local socket round-trip " , localSocketRoundTrip) ,
0 commit comments