@@ -52,6 +52,10 @@ const SAMPLE_TEST_ID_FAILED = 'test_8f2a4d10';
5252const SAMPLE_TEST_ID_PASSED = 'test_3a91bb02' ;
5353const SAMPLE_TEST_ID_BLOCKED = 'test_blocked_4f7a' ;
5454export const SAMPLE_RUN_ID = 'run_abc' ;
55+ // Documented sentinel for `test steps --run-id run_failed_sample --dry-run`:
56+ // keeps wait flows on the default passed sample while still demonstrating a
57+ // run-scoped failed step offline.
58+ const SAMPLE_FAILED_RUN_ID = 'run_failed_sample' ;
5559// M3.4 rerun dry-run sample IDs
5660const SAMPLE_RERUN_ID_BE_NAMED = 'run_rerun_be_named' ;
5761const SAMPLE_RERUN_ID_BE_PRODUCER = 'run_rerun_be_producer' ;
@@ -347,6 +351,118 @@ const failureSummary: CliFailureSummary = {
347351 recommendedFixTarget : failureContext . failure . recommendedFixTarget ,
348352} ;
349353
354+ const passedRunSample : RunResponse = {
355+ runId : SAMPLE_RUN_ID ,
356+ testId : SAMPLE_TEST_ID_PASSED ,
357+ projectId : SAMPLE_PROJECT_ID ,
358+ userId : SAMPLE_USER_ID ,
359+ status : 'passed' ,
360+ source : 'cli' ,
361+ createdAt : '2026-05-15T19:32:00.000Z' ,
362+ startedAt : '2026-05-15T19:32:05.000Z' ,
363+ finishedAt : '2026-05-15T19:34:00.000Z' ,
364+ codeVersion : 'v1' ,
365+ targetUrl : SAMPLE_TARGET_URL ,
366+ createdFrom : null ,
367+ failedStepIndex : null ,
368+ failureKind : null ,
369+ error : null ,
370+ videoUrl : null ,
371+ stepSummary : {
372+ total : 8 ,
373+ completed : 8 ,
374+ passedCount : 8 ,
375+ failedCount : 0 ,
376+ } ,
377+ // Representative per-run steps so `test steps --run-id <id> --dry-run`
378+ // demonstrates real output instead of an empty list (the generic
379+ // `/runs/{runId}` sample is also used by `test wait`, which ignores steps).
380+ steps : [
381+ {
382+ stepIndex : '0001' ,
383+ type : 'action' ,
384+ action : 'navigate' ,
385+ status : 'passed' ,
386+ description : 'Open the target URL' ,
387+ error : null ,
388+ screenshotUrl : null ,
389+ htmlSnapshotUrl : null ,
390+ createdAt : '2026-05-15T19:32:10.000Z' ,
391+ } ,
392+ {
393+ stepIndex : '0002' ,
394+ type : 'assertion' ,
395+ action : 'assert_visible' ,
396+ status : 'passed' ,
397+ description : 'Dashboard heading is visible' ,
398+ error : null ,
399+ screenshotUrl : null ,
400+ htmlSnapshotUrl : null ,
401+ createdAt : '2026-05-15T19:32:20.000Z' ,
402+ } ,
403+ ] ,
404+ } ;
405+
406+ const failedRunSample : RunResponse = {
407+ runId : SAMPLE_FAILED_RUN_ID ,
408+ testId : SAMPLE_TEST_ID_FAILED ,
409+ projectId : SAMPLE_PROJECT_ID ,
410+ userId : SAMPLE_USER_ID ,
411+ status : 'failed' ,
412+ source : 'cli' ,
413+ createdAt : '2026-05-15T19:32:00.000Z' ,
414+ startedAt : '2026-05-15T19:32:05.000Z' ,
415+ finishedAt : '2026-05-15T19:34:00.000Z' ,
416+ codeVersion : 'v1' ,
417+ targetUrl : SAMPLE_TARGET_URL ,
418+ createdFrom : null ,
419+ failedStepIndex : 3 ,
420+ failureKind : 'assertion' ,
421+ error : 'Expected billing status badge to be visible, but it was not found.' ,
422+ videoUrl : null ,
423+ stepSummary : {
424+ total : 3 ,
425+ completed : 3 ,
426+ passedCount : 2 ,
427+ failedCount : 1 ,
428+ } ,
429+ steps : [
430+ {
431+ stepIndex : '0001' ,
432+ type : 'action' ,
433+ action : 'navigate' ,
434+ status : 'passed' ,
435+ description : 'Open the target URL' ,
436+ error : null ,
437+ screenshotUrl : null ,
438+ htmlSnapshotUrl : null ,
439+ createdAt : '2026-05-15T19:32:10.000Z' ,
440+ } ,
441+ {
442+ stepIndex : '0002' ,
443+ type : 'assertion' ,
444+ action : 'assert_visible' ,
445+ status : 'passed' ,
446+ description : 'Dashboard heading is visible' ,
447+ error : null ,
448+ screenshotUrl : null ,
449+ htmlSnapshotUrl : null ,
450+ createdAt : '2026-05-15T19:32:20.000Z' ,
451+ } ,
452+ {
453+ stepIndex : '0003' ,
454+ type : 'assertion' ,
455+ action : 'assert_visible' ,
456+ status : 'failed' ,
457+ description : 'Billing status badge is visible' ,
458+ error : 'Expected billing status badge to be visible, but it was not found.' ,
459+ screenshotUrl : null ,
460+ htmlSnapshotUrl : null ,
461+ createdAt : '2026-05-15T19:32:30.000Z' ,
462+ } ,
463+ ] ,
464+ } ;
465+
350466/**
351467 * Dry-run sample lookup keyed by OpenAPI operationId. Order matters in
352468 * {@link findSample}: more specific patterns must precede their generic
@@ -702,71 +818,12 @@ const ENTRIES: DryRunSampleEntry[] = [
702818 } ,
703819 } satisfies BatchRerunResponse ) ,
704820 // M3.3 piece-3 — GET /runs/{runId} (live status / long-poll).
705- // Use a terminal failed row with a concrete failed step so
706- // `test steps --run-id <id> --dry-run` demonstrates the run-scoped
707- // error + failedStepIndex mapping without needing live credentials.
708- entry ( 'getRun' , 'GET' , '/runs/{runId}' , {
709- runId : SAMPLE_RUN_ID ,
710- testId : SAMPLE_TEST_ID_FAILED ,
711- projectId : SAMPLE_PROJECT_ID ,
712- userId : SAMPLE_USER_ID ,
713- status : 'failed' ,
714- source : 'cli' ,
715- createdAt : '2026-05-15T19:32:00.000Z' ,
716- startedAt : '2026-05-15T19:32:05.000Z' ,
717- finishedAt : '2026-05-15T19:34:00.000Z' ,
718- codeVersion : 'v1' ,
719- targetUrl : SAMPLE_TARGET_URL ,
720- createdFrom : null ,
721- failedStepIndex : 3 ,
722- failureKind : 'assertion' ,
723- error : 'Expected billing status badge to be visible, but it was not found.' ,
724- videoUrl : null ,
725- stepSummary : {
726- total : 3 ,
727- completed : 3 ,
728- passedCount : 2 ,
729- failedCount : 1 ,
730- } ,
731- // Representative per-run steps so `test steps --run-id <id> --dry-run`
732- // demonstrates real output instead of an empty list (the generic
733- // `/runs/{runId}` sample is also safe for wait flows, which ignore steps).
734- steps : [
735- {
736- stepIndex : '0001' ,
737- type : 'action' ,
738- action : 'navigate' ,
739- status : 'passed' ,
740- description : 'Open the target URL' ,
741- error : null ,
742- screenshotUrl : null ,
743- htmlSnapshotUrl : null ,
744- createdAt : '2026-05-15T19:32:10.000Z' ,
745- } ,
746- {
747- stepIndex : '0002' ,
748- type : 'assertion' ,
749- action : 'assert_visible' ,
750- status : 'passed' ,
751- description : 'Dashboard heading is visible' ,
752- error : null ,
753- screenshotUrl : null ,
754- htmlSnapshotUrl : null ,
755- createdAt : '2026-05-15T19:32:20.000Z' ,
756- } ,
757- {
758- stepIndex : '0003' ,
759- type : 'assertion' ,
760- action : 'assert_visible' ,
761- status : 'failed' ,
762- description : 'Billing status badge is visible' ,
763- error : 'Expected billing status badge to be visible, but it was not found.' ,
764- screenshotUrl : null ,
765- htmlSnapshotUrl : null ,
766- createdAt : '2026-05-15T19:32:30.000Z' ,
767- } ,
768- ] ,
769- } satisfies RunResponse ) ,
821+ // A terminal `passed` row is the most useful dry-run shape: agents see
822+ // what a completed run looks like, and `--wait` terminates immediately.
823+ // fix(2026-05-21): a duplicate failed-shape entry that appeared before
824+ // this entry was removed; findSample first-match-wins was always
825+ // returning status: "failed" for `test wait --dry-run`.
826+ entry ( 'getRun' , 'GET' , '/runs/{runId}' , passedRunSample ) ,
770827 // DEV-331 piece 3 — POST /runs/{runId}/cancel. Method-guarded in
771828 // `findSample` (POST vs `getRun`'s GET), so this can't be shadowed by the
772829 // broader `/runs/{runId}` pattern above despite sharing its path prefix.
@@ -848,11 +905,15 @@ export function findSample(
848905 const pathOnly = extractPath ( url ) ;
849906 for ( const e of ENTRIES ) {
850907 if ( e . method === upper && e . pattern . test ( pathOnly ) ) {
908+ const body =
909+ e . operationId === 'getRun' && pathOnly === `/runs/${ SAMPLE_FAILED_RUN_ID } `
910+ ? failedRunSample
911+ : e . body ( requestBody ) ;
851912 // Rebind body so callers get the resolved value, not the factory.
852913 // We return a new object with `body` already applied so downstream
853914 // code can keep calling `e.body` as-before (no API break for tests
854915 // that call `findSample` directly).
855- return { ...e , body : ( ) => e . body ( requestBody ) } ;
916+ return { ...e , body : ( ) => body } ;
856917 }
857918 }
858919 return undefined ;
0 commit comments