Skip to content

Commit 6b6de67

Browse files
committed
fix(dry-run): use sentinel failed run sample
1 parent 0a0adbc commit 6b6de67

3 files changed

Lines changed: 147 additions & 69 deletions

File tree

src/commands/test.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3025,7 +3025,7 @@ describe('runSteps', () => {
30253025
expect(block.match(/error: /g)).toHaveLength(1);
30263026
});
30273027

3028-
it('--run-id dry-run sample maps the failed step error and failure contributor flag', async () => {
3028+
it('--run-id run_failed_sample dry-run sample maps the failed step error and contributor flag', async () => {
30293029
const out: string[] = [];
30303030
const page = await runSteps(
30313031
{
@@ -3034,7 +3034,7 @@ describe('runSteps', () => {
30343034
debug: false,
30353035
dryRun: true,
30363036
testId: 'test_fe',
3037-
runId: 'run_dry',
3037+
runId: 'run_failed_sample',
30383038
},
30393039
{
30403040
env: {} as NodeJS.ProcessEnv,

src/lib/dry-run/samples.test.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,9 +424,25 @@ describe('findSample', () => {
424424
expect(e).toBeUndefined();
425425
});
426426

427-
it('GET /runs/{runId} sample includes a failed run-scoped step', () => {
427+
// defect-2 fix: getRun sample must return the passed shape (first-match-wins
428+
// in findSample). Prior to fix, a duplicate failed-shape entry appeared
429+
// before the passed-shape entry; `test wait --dry-run` always resolved to
430+
// status: "failed", giving agents the wrong happy-path canned response.
431+
it('GET /runs/{runId} resolves to the passed-shape getRun (not the failed shape)', () => {
428432
const e = findSample('GET', 'https://api.testsprite.com/api/cli/v1/runs/run_xyz');
429433
expect(e?.operationId).toBe('getRun');
434+
const body = e?.body() as {
435+
status: string;
436+
runId: string;
437+
stepSummary: { failedCount: number };
438+
};
439+
expect(body.status).toBe('passed');
440+
expect(body.stepSummary.failedCount).toBe(0);
441+
});
442+
443+
it('GET /runs/run_failed_sample resolves to the sentinel failed run-scoped step sample', () => {
444+
const e = findSample('GET', 'https://api.testsprite.com/api/cli/v1/runs/run_failed_sample');
445+
expect(e?.operationId).toBe('getRun');
430446
const body = e?.body() as {
431447
status: string;
432448
runId: string;
@@ -441,6 +457,7 @@ describe('findSample', () => {
441457
error: string | null;
442458
}>;
443459
};
460+
expect(body.runId).toBe('run_failed_sample');
444461
expect(body.status).toBe('failed');
445462
expect(body.failedStepIndex).toBe(3);
446463
expect(body.failureKind).toBe('assertion');

src/lib/dry-run/samples.ts

Lines changed: 127 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ const SAMPLE_TEST_ID_FAILED = 'test_8f2a4d10';
5252
const SAMPLE_TEST_ID_PASSED = 'test_3a91bb02';
5353
const SAMPLE_TEST_ID_BLOCKED = 'test_blocked_4f7a';
5454
export 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
5660
const SAMPLE_RERUN_ID_BE_NAMED = 'run_rerun_be_named';
5761
const 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

Comments
 (0)