@@ -47,6 +47,10 @@ const SAMPLE_TEST_ID_FAILED = 'test_8f2a4d10';
4747const SAMPLE_TEST_ID_PASSED = 'test_3a91bb02' ;
4848const SAMPLE_TEST_ID_BLOCKED = 'test_blocked_4f7a' ;
4949export const SAMPLE_RUN_ID = 'run_abc' ;
50+ // Documented sentinel for `test steps --run-id run_failed_sample --dry-run`:
51+ // keeps wait flows on the default passed sample while still demonstrating a
52+ // run-scoped failed step offline.
53+ const SAMPLE_FAILED_RUN_ID = 'run_failed_sample' ;
5054// M3.4 rerun dry-run sample IDs
5155const SAMPLE_RERUN_ID_BE_NAMED = 'run_rerun_be_named' ;
5256const SAMPLE_RERUN_ID_BE_PRODUCER = 'run_rerun_be_producer' ;
@@ -341,6 +345,118 @@ const failureSummary: CliFailureSummary = {
341345 recommendedFixTarget : failureContext . failure . recommendedFixTarget ,
342346} ;
343347
348+ const passedRunSample : RunResponse = {
349+ runId : SAMPLE_RUN_ID ,
350+ testId : SAMPLE_TEST_ID_PASSED ,
351+ projectId : SAMPLE_PROJECT_ID ,
352+ userId : SAMPLE_USER_ID ,
353+ status : 'passed' ,
354+ source : 'cli' ,
355+ createdAt : '2026-05-15T19:32:00.000Z' ,
356+ startedAt : '2026-05-15T19:32:05.000Z' ,
357+ finishedAt : '2026-05-15T19:34:00.000Z' ,
358+ codeVersion : 'v1' ,
359+ targetUrl : SAMPLE_TARGET_URL ,
360+ createdFrom : null ,
361+ failedStepIndex : null ,
362+ failureKind : null ,
363+ error : null ,
364+ videoUrl : null ,
365+ stepSummary : {
366+ total : 8 ,
367+ completed : 8 ,
368+ passedCount : 8 ,
369+ failedCount : 0 ,
370+ } ,
371+ // Representative per-run steps so `test steps --run-id <id> --dry-run`
372+ // demonstrates real output instead of an empty list (the generic
373+ // `/runs/{runId}` sample is also used by `test wait`, which ignores steps).
374+ steps : [
375+ {
376+ stepIndex : '0001' ,
377+ type : 'action' ,
378+ action : 'navigate' ,
379+ status : 'passed' ,
380+ description : 'Open the target URL' ,
381+ error : null ,
382+ screenshotUrl : null ,
383+ htmlSnapshotUrl : null ,
384+ createdAt : '2026-05-15T19:32:10.000Z' ,
385+ } ,
386+ {
387+ stepIndex : '0002' ,
388+ type : 'assertion' ,
389+ action : 'assert_visible' ,
390+ status : 'passed' ,
391+ description : 'Dashboard heading is visible' ,
392+ error : null ,
393+ screenshotUrl : null ,
394+ htmlSnapshotUrl : null ,
395+ createdAt : '2026-05-15T19:32:20.000Z' ,
396+ } ,
397+ ] ,
398+ } ;
399+
400+ const failedRunSample : RunResponse = {
401+ runId : SAMPLE_FAILED_RUN_ID ,
402+ testId : SAMPLE_TEST_ID_FAILED ,
403+ projectId : SAMPLE_PROJECT_ID ,
404+ userId : SAMPLE_USER_ID ,
405+ status : 'failed' ,
406+ source : 'cli' ,
407+ createdAt : '2026-05-15T19:32:00.000Z' ,
408+ startedAt : '2026-05-15T19:32:05.000Z' ,
409+ finishedAt : '2026-05-15T19:34:00.000Z' ,
410+ codeVersion : 'v1' ,
411+ targetUrl : SAMPLE_TARGET_URL ,
412+ createdFrom : null ,
413+ failedStepIndex : 3 ,
414+ failureKind : 'assertion' ,
415+ error : 'Expected billing status badge to be visible, but it was not found.' ,
416+ videoUrl : null ,
417+ stepSummary : {
418+ total : 3 ,
419+ completed : 3 ,
420+ passedCount : 2 ,
421+ failedCount : 1 ,
422+ } ,
423+ steps : [
424+ {
425+ stepIndex : '0001' ,
426+ type : 'action' ,
427+ action : 'navigate' ,
428+ status : 'passed' ,
429+ description : 'Open the target URL' ,
430+ error : null ,
431+ screenshotUrl : null ,
432+ htmlSnapshotUrl : null ,
433+ createdAt : '2026-05-15T19:32:10.000Z' ,
434+ } ,
435+ {
436+ stepIndex : '0002' ,
437+ type : 'assertion' ,
438+ action : 'assert_visible' ,
439+ status : 'passed' ,
440+ description : 'Dashboard heading is visible' ,
441+ error : null ,
442+ screenshotUrl : null ,
443+ htmlSnapshotUrl : null ,
444+ createdAt : '2026-05-15T19:32:20.000Z' ,
445+ } ,
446+ {
447+ stepIndex : '0003' ,
448+ type : 'assertion' ,
449+ action : 'assert_visible' ,
450+ status : 'failed' ,
451+ description : 'Billing status badge is visible' ,
452+ error : 'Expected billing status badge to be visible, but it was not found.' ,
453+ screenshotUrl : null ,
454+ htmlSnapshotUrl : null ,
455+ createdAt : '2026-05-15T19:32:30.000Z' ,
456+ } ,
457+ ] ,
458+ } ;
459+
344460/**
345461 * Dry-run sample lookup keyed by OpenAPI operationId. Order matters in
346462 * {@link findSample}: more specific patterns must precede their generic
@@ -691,71 +807,12 @@ const ENTRIES: DryRunSampleEntry[] = [
691807 } ,
692808 } satisfies BatchRerunResponse ) ,
693809 // M3.3 piece-3 — GET /runs/{runId} (live status / long-poll).
694- // Use a terminal failed row with a concrete failed step so
695- // `test steps --run-id <id> --dry-run` demonstrates the run-scoped
696- // error + failedStepIndex mapping without needing live credentials.
697- entry ( 'getRun' , 'GET' , '/runs/{runId}' , {
698- runId : SAMPLE_RUN_ID ,
699- testId : SAMPLE_TEST_ID_FAILED ,
700- projectId : SAMPLE_PROJECT_ID ,
701- userId : SAMPLE_USER_ID ,
702- status : 'failed' ,
703- source : 'cli' ,
704- createdAt : '2026-05-15T19:32:00.000Z' ,
705- startedAt : '2026-05-15T19:32:05.000Z' ,
706- finishedAt : '2026-05-15T19:34:00.000Z' ,
707- codeVersion : 'v1' ,
708- targetUrl : SAMPLE_TARGET_URL ,
709- createdFrom : null ,
710- failedStepIndex : 3 ,
711- failureKind : 'assertion' ,
712- error : 'Expected billing status badge to be visible, but it was not found.' ,
713- videoUrl : null ,
714- stepSummary : {
715- total : 3 ,
716- completed : 3 ,
717- passedCount : 2 ,
718- failedCount : 1 ,
719- } ,
720- // Representative per-run steps so `test steps --run-id <id> --dry-run`
721- // demonstrates real output instead of an empty list (the generic
722- // `/runs/{runId}` sample is also safe for wait flows, which ignore steps).
723- steps : [
724- {
725- stepIndex : '0001' ,
726- type : 'action' ,
727- action : 'navigate' ,
728- status : 'passed' ,
729- description : 'Open the target URL' ,
730- error : null ,
731- screenshotUrl : null ,
732- htmlSnapshotUrl : null ,
733- createdAt : '2026-05-15T19:32:10.000Z' ,
734- } ,
735- {
736- stepIndex : '0002' ,
737- type : 'assertion' ,
738- action : 'assert_visible' ,
739- status : 'passed' ,
740- description : 'Dashboard heading is visible' ,
741- error : null ,
742- screenshotUrl : null ,
743- htmlSnapshotUrl : null ,
744- createdAt : '2026-05-15T19:32:20.000Z' ,
745- } ,
746- {
747- stepIndex : '0003' ,
748- type : 'assertion' ,
749- action : 'assert_visible' ,
750- status : 'failed' ,
751- description : 'Billing status badge is visible' ,
752- error : 'Expected billing status badge to be visible, but it was not found.' ,
753- screenshotUrl : null ,
754- htmlSnapshotUrl : null ,
755- createdAt : '2026-05-15T19:32:30.000Z' ,
756- } ,
757- ] ,
758- } satisfies RunResponse ) ,
810+ // A terminal `passed` row is the most useful dry-run shape: agents see
811+ // what a completed run looks like, and `--wait` terminates immediately.
812+ // fix(2026-05-21): a duplicate failed-shape entry that appeared before
813+ // this entry was removed; findSample first-match-wins was always
814+ // returning status: "failed" for `test wait --dry-run`.
815+ entry ( 'getRun' , 'GET' , '/runs/{runId}' , passedRunSample ) ,
759816] ;
760817
761818function entry (
@@ -807,11 +864,15 @@ export function findSample(
807864 const pathOnly = extractPath ( url ) ;
808865 for ( const e of ENTRIES ) {
809866 if ( e . method === upper && e . pattern . test ( pathOnly ) ) {
867+ const body =
868+ e . operationId === 'getRun' && pathOnly === `/runs/${ SAMPLE_FAILED_RUN_ID } `
869+ ? failedRunSample
870+ : e . body ( requestBody ) ;
810871 // Rebind body so callers get the resolved value, not the factory.
811872 // We return a new object with `body` already applied so downstream
812873 // code can keep calling `e.body` as-before (no API break for tests
813874 // that call `findSample` directly).
814- return { ...e , body : ( ) => e . body ( requestBody ) } ;
875+ return { ...e , body : ( ) => body } ;
815876 }
816877 }
817878 return undefined ;
0 commit comments