Skip to content

Commit 9179281

Browse files
committed
fix(dry-run): use sentinel failed run sample
1 parent 4b2e45b commit 9179281

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
@@ -2667,7 +2667,7 @@ describe('runSteps', () => {
26672667
expect(block.match(/error: /g)).toHaveLength(1);
26682668
});
26692669

2670-
it('--run-id dry-run sample maps the failed step error and failure contributor flag', async () => {
2670+
it('--run-id run_failed_sample dry-run sample maps the failed step error and contributor flag', async () => {
26712671
const out: string[] = [];
26722672
const page = await runSteps(
26732673
{
@@ -2676,7 +2676,7 @@ describe('runSteps', () => {
26762676
debug: false,
26772677
dryRun: true,
26782678
testId: 'test_fe',
2679-
runId: 'run_dry',
2679+
runId: 'run_failed_sample',
26802680
},
26812681
{
26822682
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
@@ -407,9 +407,25 @@ describe('findSample', () => {
407407
expect(e).toBeUndefined();
408408
});
409409

410-
it('GET /runs/{runId} sample includes a failed run-scoped step', () => {
410+
// defect-2 fix: getRun sample must return the passed shape (first-match-wins
411+
// in findSample). Prior to fix, a duplicate failed-shape entry appeared
412+
// before the passed-shape entry; `test wait --dry-run` always resolved to
413+
// status: "failed", giving agents the wrong happy-path canned response.
414+
it('GET /runs/{runId} resolves to the passed-shape getRun (not the failed shape)', () => {
411415
const e = findSample('GET', 'https://api.testsprite.com/api/cli/v1/runs/run_xyz');
412416
expect(e?.operationId).toBe('getRun');
417+
const body = e?.body() as {
418+
status: string;
419+
runId: string;
420+
stepSummary: { failedCount: number };
421+
};
422+
expect(body.status).toBe('passed');
423+
expect(body.stepSummary.failedCount).toBe(0);
424+
});
425+
426+
it('GET /runs/run_failed_sample resolves to the sentinel failed run-scoped step sample', () => {
427+
const e = findSample('GET', 'https://api.testsprite.com/api/cli/v1/runs/run_failed_sample');
428+
expect(e?.operationId).toBe('getRun');
413429
const body = e?.body() as {
414430
status: string;
415431
runId: string;
@@ -424,6 +440,7 @@ describe('findSample', () => {
424440
error: string | null;
425441
}>;
426442
};
443+
expect(body.runId).toBe('run_failed_sample');
427444
expect(body.status).toBe('failed');
428445
expect(body.failedStepIndex).toBe(3);
429446
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
@@ -47,6 +47,10 @@ const SAMPLE_TEST_ID_FAILED = 'test_8f2a4d10';
4747
const SAMPLE_TEST_ID_PASSED = 'test_3a91bb02';
4848
const SAMPLE_TEST_ID_BLOCKED = 'test_blocked_4f7a';
4949
export 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
5155
const SAMPLE_RERUN_ID_BE_NAMED = 'run_rerun_be_named';
5256
const 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

761818
function 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

Comments
 (0)