@@ -114,10 +114,9 @@ function executionForDetail(dumps, detail) {
114114 return null ;
115115}
116116
117- function evidenceForStep ( step , embedded ) {
117+ function evidenceForStep ( step , embedded , allowUntimedEvidence = false ) {
118118 const candidates = [ ] ;
119- for ( const detail of step . agentDetails ?? [ ] ) {
120- const execution = executionForDetail ( embedded . dumps , detail ) ;
119+ function collect ( execution , destination ) {
121120 for ( const task of execution ?. tasks ?? [ ] ) {
122121 const screenshotId = task ?. uiContext ?. screenshot ?. id ;
123122 const explanation = modelTaskText ( task ) ;
@@ -126,17 +125,42 @@ function evidenceForStep(step, embedded) {
126125 screenshotId &&
127126 embedded . images . has ( screenshotId )
128127 ) {
129- candidates . push ( { screenshotId, explanation } ) ;
128+ destination . push ( { screenshotId, explanation } ) ;
130129 }
131130 }
132131 }
132+ for ( const detail of step . agentDetails ?? [ ] ) {
133+ collect ( executionForDetail ( embedded . dumps , detail ) , candidates ) ;
134+ }
135+ // Custom Nodes can call an Agent without forwarding agentDetails to the
136+ // runner step. Match by execution time when available; for older reports
137+ // without timestamps, only a one-step, one-attempt case is unambiguous.
138+ if ( ! candidates . length && ! hasScreenshotEvidence ( step ) ) {
139+ const startedAt = Date . parse ( step . startedAt ?? '' ) ;
140+ const endedAt = Date . parse ( step . endedAt ?? '' ) ;
141+ const hasWindow = Number . isFinite ( startedAt ) && Number . isFinite ( endedAt ) ;
142+ const unlinked = [ ] ;
143+ for ( const entry of embedded . dumps ) {
144+ for ( const execution of entry . dump ?. executions ?? [ ] ) {
145+ const executionTime = Number (
146+ execution . logTime ?? execution . tasks ?. [ 0 ] ?. timing ?. start ,
147+ ) ;
148+ const inStep = hasWindow && Number . isFinite ( executionTime ) &&
149+ executionTime >= startedAt - 1000 &&
150+ executionTime <= endedAt + 1000 ;
151+ if ( ! inStep && ! ( allowUntimedEvidence && ! hasWindow ) ) continue ;
152+ collect ( execution , unlinked ) ;
153+ }
154+ }
155+ if ( unlinked . length === 1 ) candidates . push ( unlinked [ 0 ] ) ;
156+ }
133157 const selected = candidates . at ( - 1 ) ;
134158 const error = normalizedText ( step . error ?. message ) ;
135159 const result = normalizedText ( step . output ?. summary ) ;
136- const description = error ?? selected ?. explanation ?? result ;
137- if ( ! description ) {
138- throw new Error ( ` Step ${ step . id } has no AI response or error text` ) ;
139- }
160+ const description = error ?? selected ?. explanation ?? result ??
161+ ( step . status === 'success'
162+ ? ' Step passed; no per-step AI response was recorded.'
163+ : 'Step failed without a recorded error message.' ) ;
140164 return {
141165 ...( selected
142166 ? { screenshot : embedded . images . get ( selected . screenshotId ) }
@@ -159,61 +183,71 @@ export async function reportCases(
159183 const embedded = reportHtml
160184 ? await embeddedReportData ( reportHtml , reportFile )
161185 : null ;
162- return ( project . documents ?? [ ] ) . flatMap ( ( document ) =>
163- ( document . cases ?? [ ] ) . flatMap ( ( testCase ) => {
164- const attempt = testCase . attempts ?. at ( - 1 ) ;
165- if ( ! attempt ) {
166- if ( testCase . status === 'not-run' ) return [ ] ;
167- throw new Error (
168- `Case ${ testCase . name ?? testCase . caseId } has no attempt` ,
169- ) ;
170- }
171- const steps = allAttemptSteps ( attempt ) ;
172- const passed = ( testCase . status ?? attempt . status ) === 'success' ;
173- const step = passed
174- ? steps . findLast ( hasScreenshotEvidence ) ?? steps . at ( - 1 )
175- : steps . find (
176- ( item ) => item . status === 'failed' && hasScreenshotEvidence ( item ) ,
177- ) ??
178- steps . find ( ( item ) => item . status === 'failed' ) ;
179- if ( ! step ?. id ) {
180- throw new Error (
181- `Case ${ testCase . name ?? testCase . caseId } has no report step to preview` ,
182- ) ;
183- }
184- if ( ! testCase . caseId || ! testCase . name ) {
185- throw new Error ( 'Midscene case metadata is incomplete' ) ;
186- }
187- const evidence = embedded ? evidenceForStep ( step , embedded ) : null ;
188- if ( passed && embedded && ! evidence ?. screenshot ) {
189- throw new Error ( `Step ${ step . id } has no embedded node screenshot` ) ;
190- }
191- const selection = passed
192- ? 'last-screenshot'
193- : evidence && ! evidence . screenshot
194- ? 'first-failing-no-screenshot'
195- : 'first-failing-screenshot' ;
196- return [ {
197- caseId : testCase . caseId ,
198- name : testCase . name ,
199- status : passed ? 'success' : 'failed' ,
200- durationMs : attempt . durationMs ,
201- stepId : step . id ,
202- stepTitle : step . title ?? step . node ,
203- selection,
204- ...( evidence ?. screenshot
205- ? {
206- previewFile : casePreviewFileName (
207- projectName ,
208- testCase . caseId ,
209- evidence . screenshot . extension ,
210- ) ,
211- }
212- : embedded
213- ? { }
214- : { previewFile : casePreviewFileName ( projectName , testCase . caseId ) } ) ,
215- ...( evidence ?? { } ) ,
216- } ] ;
217- } ) ,
186+ const cases = ( project . documents ?? [ ] ) . flatMap ( ( document ) =>
187+ document . cases ?? [ ] ,
218188 ) ;
189+ return cases . flatMap ( ( testCase ) => {
190+ const attempt = testCase . attempts ?. at ( - 1 ) ;
191+ if ( ! attempt ) {
192+ if ( testCase . status === 'not-run' ) return [ ] ;
193+ throw new Error (
194+ `Case ${ testCase . name ?? testCase . caseId } has no attempt` ,
195+ ) ;
196+ }
197+ const steps = allAttemptSteps ( attempt ) ;
198+ const passed = ( testCase . status ?? attempt . status ) === 'success' ;
199+ const step = passed
200+ ? steps . findLast ( hasScreenshotEvidence ) ?? steps . at ( - 1 )
201+ : steps . find (
202+ ( item ) => item . status === 'failed' && hasScreenshotEvidence ( item ) ,
203+ ) ??
204+ steps . find ( ( item ) => item . status === 'failed' ) ;
205+ if ( ! step ?. id ) {
206+ throw new Error (
207+ `Case ${ testCase . name ?? testCase . caseId } has no report step to preview` ,
208+ ) ;
209+ }
210+ if ( ! testCase . caseId || ! testCase . name ) {
211+ throw new Error ( 'Midscene case metadata is incomplete' ) ;
212+ }
213+ const soleUntimedStep = cases . length === 1 &&
214+ testCase . attempts ?. length === 1 && steps . length === 1 ;
215+ const evidence = embedded
216+ ? evidenceForStep ( step , embedded , soleUntimedStep )
217+ : null ;
218+ if (
219+ passed && embedded && ! evidence ?. screenshot &&
220+ ( hasScreenshotEvidence ( step ) || step . node ?. startsWith ( 'ai' ) )
221+ ) {
222+ throw new Error ( `Step ${ step . id } has no embedded node screenshot` ) ;
223+ }
224+ const selection = passed
225+ ? embedded && ! evidence ?. screenshot
226+ ? 'last-no-screenshot'
227+ : 'last-screenshot'
228+ : evidence && ! evidence . screenshot
229+ ? 'first-failing-no-screenshot'
230+ : 'first-failing-screenshot' ;
231+ return [ {
232+ caseId : testCase . caseId ,
233+ name : testCase . name ,
234+ status : passed ? 'success' : 'failed' ,
235+ durationMs : attempt . durationMs ,
236+ stepId : step . id ,
237+ stepTitle : step . title ?? step . node ,
238+ selection,
239+ ...( evidence ?. screenshot
240+ ? {
241+ previewFile : casePreviewFileName (
242+ projectName ,
243+ testCase . caseId ,
244+ evidence . screenshot . extension ,
245+ ) ,
246+ }
247+ : embedded
248+ ? { }
249+ : { previewFile : casePreviewFileName ( projectName , testCase . caseId ) } ) ,
250+ ...( evidence ?? { } ) ,
251+ } ] ;
252+ } ) ;
219253}
0 commit comments