@@ -376,68 +376,100 @@ async function writeCapturesToDisk(
376376 }
377377}
378378
379+ /**
380+ * Produces a human-readable explanation of why no stored conversation matched
381+ * a given request. For each stored conversation it reports the first reason
382+ * matching failed, mirroring the logic in {@link findAssistantIndexAfterPrefix}.
383+ */
384+ function diagnoseMatchFailure (
385+ requestMessages : NormalizedMessage [ ] ,
386+ rawMessages : unknown [ ] ,
387+ storedData : NormalizedData | undefined ,
388+ ) : string {
389+ const lines : string [ ] = [ ] ;
390+ lines . push ( `Request has ${ requestMessages . length } normalized messages (${ rawMessages . length } raw).` ) ;
391+
392+ if ( ! storedData || storedData . conversations . length === 0 ) {
393+ lines . push ( "No stored conversations to match against." ) ;
394+ return lines . join ( "\n" ) ;
395+ }
396+
397+ for ( let c = 0 ; c < storedData . conversations . length ; c ++ ) {
398+ const saved = storedData . conversations [ c ] . messages ;
399+
400+ // Same check as findAssistantIndexAfterPrefix: request must be a strict prefix
401+ if ( requestMessages . length >= saved . length ) {
402+ lines . push (
403+ `Conversation ${ c } (${ saved . length } messages): ` +
404+ `skipped — request has ${ requestMessages . length } messages, need fewer than ${ saved . length } .` ,
405+ ) ;
406+ continue ;
407+ }
408+
409+ // Find the first message that doesn't match
410+ let mismatchIndex = - 1 ;
411+ for ( let i = 0 ; i < requestMessages . length ; i ++ ) {
412+ if ( JSON . stringify ( requestMessages [ i ] ) !== JSON . stringify ( saved [ i ] ) ) {
413+ mismatchIndex = i ;
414+ break ;
415+ }
416+ }
417+
418+ if ( mismatchIndex >= 0 ) {
419+ const raw = mismatchIndex < rawMessages . length
420+ ? JSON . stringify ( rawMessages [ mismatchIndex ] ) . slice ( 0 , 300 )
421+ : "(no raw message)" ;
422+ lines . push (
423+ `Conversation ${ c } (${ saved . length } messages): mismatch at message ${ mismatchIndex } :` ,
424+ ` request: ${ JSON . stringify ( requestMessages [ mismatchIndex ] ) . slice ( 0 , 200 ) } ` ,
425+ ` saved: ${ JSON . stringify ( saved [ mismatchIndex ] ) . slice ( 0 , 200 ) } ` ,
426+ ` raw (pre-normalization): ${ raw } ` ,
427+ ) ;
428+ } else {
429+ // Prefix matched, but the next saved message isn't an assistant turn
430+ const nextRole = saved [ requestMessages . length ] ?. role ?? "(end of conversation)" ;
431+ lines . push (
432+ `Conversation ${ c } (${ saved . length } messages): ` +
433+ `prefix matched, but next saved message is "${ nextRole } " (need "assistant").` ,
434+ ) ;
435+ }
436+ }
437+
438+ return lines . join ( "\n" ) ;
439+ }
440+
379441async function exitWithNoMatchingRequestError (
380442 options : PerformRequestOptions ,
381443 testInfo : { file : string ; line ?: number } | undefined ,
382444 workDir : string ,
383445 toolResultNormalizers : ToolResultNormalizer [ ] ,
384446 storedData ?: NormalizedData ,
385447) {
386- const parts : string [ ] = [ ] ;
387- if ( testInfo ?. file ) parts . push ( `file=${ testInfo . file } ` ) ;
388- if ( typeof testInfo ?. line === "number" ) parts . push ( `line=${ testInfo . line } ` ) ;
389- const header = parts . length ? ` ${ parts . join ( "," ) } ` : "" ;
390-
391- let diagnostics = "" ;
448+ let diagnostics : string ;
392449 try {
393- const normalized = await parseAndNormalizeRequest (
394- options . body ,
395- workDir ,
396- toolResultNormalizers ,
397- ) ;
450+ const normalized = await parseAndNormalizeRequest ( options . body , workDir , toolResultNormalizers ) ;
398451 const requestMessages = normalized . conversations [ 0 ] ?. messages ?? [ ] ;
399452
400- // Also parse raw messages to see what normalization drops
401453 let rawMessages : unknown [ ] = [ ] ;
402454 try {
403- const parsed = JSON . parse ( options . body ?? "{}" ) as { messages ?: unknown [ ] } ;
404- rawMessages = parsed . messages ?? [ ] ;
405- } catch { /* ignore */ }
406-
407- diagnostics += `Request has ${ requestMessages . length } normalized messages (${ rawMessages . length } raw).\n` ;
408-
409- if ( storedData ) {
410- for ( let c = 0 ; c < storedData . conversations . length ; c ++ ) {
411- const saved = storedData . conversations [ c ] . messages ;
412- diagnostics += `Conversation ${ c } has ${ saved . length } messages. ` ;
413- if ( requestMessages . length >= saved . length ) {
414- diagnostics += `Skipped: request (${ requestMessages . length } ) >= saved (${ saved . length } ).\n` ;
415- continue ;
416- }
417- let mismatchAt = - 1 ;
418- for ( let i = 0 ; i < requestMessages . length ; i ++ ) {
419- const reqMsg = JSON . stringify ( requestMessages [ i ] ) ;
420- const savedMsg = JSON . stringify ( saved [ i ] ) ;
421- if ( reqMsg !== savedMsg ) {
422- mismatchAt = i ;
423- const rawMsg = i < rawMessages . length ? JSON . stringify ( rawMessages [ i ] ) . slice ( 0 , 300 ) : "(no raw)" ;
424- diagnostics += `Mismatch at message ${ i } :\n normalized: ${ reqMsg . slice ( 0 , 200 ) } \n saved: ${ savedMsg . slice ( 0 , 200 ) } \n raw: ${ rawMsg } \n` ;
425- break ;
426- }
427- }
428- if ( mismatchAt === - 1 ) {
429- const nextRole = saved [ requestMessages . length ] ?. role ;
430- diagnostics += `Prefix matched but next message role is "${ nextRole } " (need "assistant").\n` ;
431- }
432- }
433- }
455+ rawMessages = ( JSON . parse ( options . body ?? "{}" ) as { messages ?: unknown [ ] } ) . messages ?? [ ] ;
456+ } catch { /* non-JSON body */ }
457+
458+ diagnostics = diagnoseMatchFailure ( requestMessages , rawMessages , storedData ) ;
434459 } catch ( e ) {
435- diagnostics = `(unable to parse request: ${ e } )` ;
460+ diagnostics = `(unable to parse request for diagnostics : ${ e } )` ;
436461 }
437462
438463 const errorMessage =
439464 `No cached response found for ${ options . requestOptions . method } ${ options . requestOptions . path } .\n${ diagnostics } ` ;
440- process . stderr . write ( `::error${ header } ::${ errorMessage } \n` ) ;
465+
466+ // Format as GitHub Actions annotation when test location is available
467+ const annotation = [
468+ testInfo ?. file ? `file=${ testInfo . file } ` : "" ,
469+ typeof testInfo ?. line === "number" ? `line=${ testInfo . line } ` : "" ,
470+ ] . filter ( Boolean ) . join ( "," ) ;
471+ process . stderr . write ( `::error${ annotation ? ` ${ annotation } ` : "" } ::${ errorMessage } \n` ) ;
472+
441473 options . onError ( new Error ( errorMessage ) ) ;
442474}
443475
0 commit comments