@@ -79,7 +79,7 @@ const mailbox: GmailMessage[] = Array.from({ length: 12 }, (_, index) => ({
7979 body : { data : Buffer . from ( `Synthetic evidence ${ index } .` ) . toString ( 'base64url' ) } ,
8080 } ,
8181} ) )
82- const messageFailures = new Map < string , number > ( )
82+ const messageFailures = new Map < string , { status : number ; format : string } > ( )
8383const mediaStarted = createDeferred < void > ( )
8484const mediaClosed = createDeferred < void > ( )
8585const server = http . createServer ( ( request , response ) => {
@@ -93,13 +93,12 @@ const server = http.createServer((request, response) => {
9393 const row = mailbox . find ( ( message ) => message . id === id )
9494 const isThread = url . pathname . startsWith ( '/gmail/v1/users/me/threads/' )
9595 const isAttachment = url . pathname . includes ( '/attachments/' )
96- const failure =
97- ! isThread && url . searchParams . get ( 'format' ) === 'full' ? messageFailures . get ( id ) : undefined
98- if ( failure ) {
99- requests . push ( { id, resource : 'message' , status : failure } )
96+ const failure = ! isThread ? messageFailures . get ( id ) : undefined
97+ if ( failure && url . searchParams . get ( 'format' ) === failure . format ) {
98+ requests . push ( { id, resource : 'message' , status : failure . status } )
10099 response
101- . writeHead ( failure , { 'Content-Type' : 'application/json' } )
102- . end ( JSON . stringify ( { error : { code : failure } } ) )
100+ . writeHead ( failure . status , { 'Content-Type' : 'application/json' } )
101+ . end ( JSON . stringify ( { error : { code : failure . status } } ) )
103102 return
104103 }
105104 const threadMessages = isThread ? mailbox . filter ( ( message ) => message . threadId === id ) : [ ]
@@ -303,68 +302,71 @@ try {
303302 { name : 'rate limit' , failedId : 'reply' , status : 429 , expectedStatus : 'rate_limited' } ,
304303 { name : 'server failure' , failedId : 'reply' , status : 500 , expectedStatus : 'unavailable' } ,
305304 ] ) {
306- await check (
307- `Gmail conversation handles ${ name } without hiding unrelated failures` ,
308- async ( ) => {
309- const before = mailbox . length
310- mailbox . push (
311- ...[ 'anchor' , 'retained' , 'reply' ] . map ( ( id , index ) => ( {
312- id,
313- threadId : 'failure-conversation' ,
314- labelIds : [ 'INBOX' ] ,
315- internalDate : String ( Date . UTC ( 2026 , 0 , 1 , 12 , index ) ) ,
316- payload : {
317- mimeType : 'text/plain' ,
318- body : { data : Buffer . from ( `Synthetic ${ id } evidence.` ) . toString ( 'base64url' ) } ,
319- } ,
320- } ) )
321- )
322- if ( failedId ) messageFailures . set ( failedId , status )
323- try {
324- const signal = AbortSignal . timeout ( 5000 )
325- const api = createNativeClient ( { origin, accessToken : TOKEN , signal } )
326- const policy = {
327- ...defaultLiveSearchPolicy ( 'gmail' ) ,
328- mode : 'selected' as const ,
329- included : [ 'INBOX' ] ,
330- }
331- const verify = createPolicyVerifier ( 'gmail' , policy , api , origin )
332- assert . ok ( await verify ( { id : 'anchor' } ) )
333- const reading = readGmail ( api , 'anchor' , {
334- policy,
335- signal,
336- verify : ( reference ) => verify ( reference , reference . accessMetadata ) ,
337- } )
338- if ( expectedStatus ) {
339- await assert . rejects (
340- reading ,
341- ( error : unknown ) =>
342- error instanceof NativeSearchError && error . status === expectedStatus
305+ for ( const format of failedId === 'reply' ? [ 'metadata' , 'full' ] : [ 'full' ] ) {
306+ await check (
307+ `Gmail conversation handles ${ name } during ${ format } reads without hiding unrelated failures` ,
308+ async ( ) => {
309+ const before = mailbox . length
310+ mailbox . push (
311+ ...[ 'anchor' , 'retained' , 'reply' ] . map ( ( id , index ) => ( {
312+ id,
313+ threadId : 'failure-conversation' ,
314+ labelIds : [ 'INBOX' ] ,
315+ internalDate : String ( Date . UTC ( 2026 , 0 , 1 , 12 , index ) ) ,
316+ payload : {
317+ mimeType : 'text/plain' ,
318+ body : { data : Buffer . from ( `Synthetic ${ id } evidence.` ) . toString ( 'base64url' ) } ,
319+ } ,
320+ } ) )
321+ )
322+ if ( failedId ) messageFailures . set ( failedId , { status, format } )
323+ try {
324+ const signal = AbortSignal . timeout ( 5000 )
325+ const api = createNativeClient ( { origin, accessToken : TOKEN , signal } )
326+ const policy = {
327+ ...defaultLiveSearchPolicy ( 'gmail' ) ,
328+ mode : 'selected' as const ,
329+ included : [ 'INBOX' ] ,
330+ }
331+ const verify = createPolicyVerifier ( 'gmail' , policy , api , origin )
332+ assert . ok ( await verify ( { id : 'anchor' } ) )
333+ const reading = readGmail ( api , 'anchor' , {
334+ policy,
335+ signal,
336+ verify : ( reference ) =>
337+ verify ( reference , format === 'full' ? reference . accessMetadata : undefined ) ,
338+ } )
339+ if ( expectedStatus ) {
340+ await assert . rejects (
341+ reading ,
342+ ( error : unknown ) =>
343+ error instanceof NativeSearchError && error . status === expectedStatus
344+ )
345+ return
346+ }
347+ const document = await reading
348+ assert . equal ( document . id , 'anchor' )
349+ assert . ok ( document . content . includes ( 'Synthetic anchor evidence.' ) )
350+ assert . ok ( document . content . includes ( 'Synthetic retained evidence.' ) )
351+ assert . equal ( document . content . includes ( 'Synthetic reply evidence.' ) , status === 200 )
352+ assert . equal ( document . content . includes ( 'Coverage incomplete:' ) , status === 404 )
353+ assert . deepEqual (
354+ document . accessDependencies ,
355+ status === 404 ? [ { id : 'retained' } ] : [ { id : 'retained' } , { id : 'reply' } ]
343356 )
344- return
357+ const current = createPolicyVerifier ( 'gmail' , policy , api , origin , undefined , {
358+ fresh : true ,
359+ } )
360+ assert . ok ( await current ( { id : document . id } ) )
361+ for ( const dependency of document . accessDependencies ?? [ ] )
362+ assert . ok ( await current ( dependency ) )
363+ } finally {
364+ mailbox . splice ( before )
365+ messageFailures . clear ( )
345366 }
346- const document = await reading
347- assert . equal ( document . id , 'anchor' )
348- assert . ok ( document . content . includes ( 'Synthetic anchor evidence.' ) )
349- assert . ok ( document . content . includes ( 'Synthetic retained evidence.' ) )
350- assert . equal ( document . content . includes ( 'Synthetic reply evidence.' ) , status === 200 )
351- assert . equal ( document . content . includes ( 'Coverage incomplete:' ) , status === 404 )
352- assert . deepEqual (
353- document . accessDependencies ,
354- status === 404 ? [ { id : 'retained' } ] : [ { id : 'retained' } , { id : 'reply' } ]
355- )
356- const current = createPolicyVerifier ( 'gmail' , policy , api , origin , undefined , {
357- fresh : true ,
358- } )
359- assert . ok ( await current ( { id : document . id } ) )
360- for ( const dependency of document . accessDependencies ?? [ ] )
361- assert . ok ( await current ( dependency ) )
362- } finally {
363- mailbox . splice ( before )
364- messageFailures . clear ( )
365367 }
366- }
367- )
368+ )
369+ }
368370 }
369371 await check (
370372 'Gmail conversation and fresh selected-label checks fit one native request budget' ,
0 commit comments