@@ -168,6 +168,33 @@ function envelope(flat: Record<string, unknown>): Record<string, unknown> {
168168 } ;
169169}
170170
171+ // This fixture preserves the field order from a real read response: the Rust wire
172+ // serializer emits `text` as the final structured field.
173+ const CAPTURED_FIRST_PARTY_READ_ENVELOPE : Record < string , unknown > = {
174+ content : [
175+ {
176+ type : "text" ,
177+ text : "export function readFixture(): string { return 'captured'; }\n" ,
178+ } ,
179+ ] ,
180+ isError : false ,
181+ structuredContent : {
182+ id : "read-fixture-1" ,
183+ success : true ,
184+ status_bar : { errors : 0 , warnings : 1 } ,
185+ bg_completions : [ { task_id : "bash-fixture-1" } ] ,
186+ text : "export function readFixture(): string { return 'captured'; }\n" ,
187+ } ,
188+ } ;
189+
190+ function withoutStructuredText ( envelopeResponse : Record < string , unknown > ) : Record < string , unknown > {
191+ const structuredContent = {
192+ ...( envelopeResponse . structuredContent as Record < string , unknown > ) ,
193+ } ;
194+ delete structuredContent . text ;
195+ return { ...envelopeResponse , structuredContent } ;
196+ }
197+
171198describe ( "SubcTransport.toolCall" , ( ) => {
172199 test ( "sends {name, arguments} and re-lifts structuredContent to the flat result" , async ( ) => {
173200 const client = new FakeClient ( async ( ) =>
@@ -436,6 +463,68 @@ describe("SubcTransport Rd reconnect", () => {
436463} ) ;
437464
438465describe ( "SubcTransport reply envelope (B-#7)" , ( ) => {
466+ test ( "synthesizes missing structuredContent.text from a single outer text block" , async ( ) => {
467+ const duplicatedClient = new FakeClient ( async ( ) => CAPTURED_FIRST_PARTY_READ_ENVELOPE ) ;
468+ const { pool : duplicatedPool } = poolWith ( duplicatedClient ) ;
469+ const duplicated = await duplicatedPool
470+ . getBridge ( "/work/proj" )
471+ . toolCall ( "sess-1" , "read" , { filePath : "fixture.ts" } ) ;
472+
473+ const synthesizedClient = new FakeClient ( async ( ) =>
474+ withoutStructuredText ( CAPTURED_FIRST_PARTY_READ_ENVELOPE ) ,
475+ ) ;
476+ const { pool : synthesizedPool } = poolWith ( synthesizedClient ) ;
477+ const synthesized = await synthesizedPool
478+ . getBridge ( "/work/proj" )
479+ . toolCall ( "sess-1" , "read" , { filePath : "fixture.ts" } ) ;
480+
481+ const capturedBytes = new TextEncoder ( ) . encode (
482+ JSON . stringify ( CAPTURED_FIRST_PARTY_READ_ENVELOPE . structuredContent ) ,
483+ ) ;
484+ expect ( new TextEncoder ( ) . encode ( JSON . stringify ( duplicated ) ) ) . toEqual ( capturedBytes ) ;
485+ expect ( new TextEncoder ( ) . encode ( JSON . stringify ( synthesized ) ) ) . toEqual ( capturedBytes ) ;
486+ } ) ;
487+
488+ test ( "present structuredContent.text wins without inspecting outer content" , async ( ) => {
489+ const response = {
490+ ...CAPTURED_FIRST_PARTY_READ_ENVELOPE ,
491+ content : [ { type : "image" , data : "not inspected" } ] ,
492+ } ;
493+ const client = new FakeClient ( async ( ) => response ) ;
494+ const { pool } = poolWith ( client ) ;
495+
496+ const result = await pool . getBridge ( "/work/proj" ) . toolCall ( "sess-1" , "read" , { } ) ;
497+
498+ expect ( new TextEncoder ( ) . encode ( JSON . stringify ( result ) ) ) . toEqual (
499+ new TextEncoder ( ) . encode (
500+ JSON . stringify ( CAPTURED_FIRST_PARTY_READ_ENVELOPE . structuredContent ) ,
501+ ) ,
502+ ) ;
503+ } ) ;
504+
505+ test . each ( [
506+ [ "zero blocks" , [ ] ] ,
507+ [
508+ "multiple blocks" ,
509+ [
510+ { type : "text" , text : "first" } ,
511+ { type : "text" , text : "second" } ,
512+ ] ,
513+ ] ,
514+ [ "non-text block" , [ { type : "image" , data : "not text" } ] ] ,
515+ ] ) ( "missing structuredContent.text with %s stays fail-closed" , async ( _name , content ) => {
516+ const client = new FakeClient ( async ( ) => ( {
517+ content,
518+ isError : false ,
519+ structuredContent : { id : "read-1" , success : true } ,
520+ } ) ) ;
521+ const { pool } = poolWith ( client ) ;
522+
523+ await expect ( pool . getBridge ( "/work/proj" ) . toolCall ( "sess-1" , "read" , { } ) ) . rejects . toThrow (
524+ "subc tool reply structuredContent lacks a boolean `success` / string `text` (protocol violation)" ,
525+ ) ;
526+ } ) ;
527+
439528 test ( "a reply missing the structuredContent envelope throws (protocol violation)" , async ( ) => {
440529 // No structuredContent → must NOT be coerced to a silent {success:false}.
441530 const client = new FakeClient ( async ( ) => ( { content : [ ] , isError : false } ) ) ;
0 commit comments