(
opts: StepsOptions,
deps: TestDeps = {},
)
| 3526 | } |
| 3527 | |
| 3528 | export async function runSteps( |
| 3529 | opts: StepsOptions, |
| 3530 | deps: TestDeps = {}, |
| 3531 | ): Promise<Page<CliTestStep>> { |
| 3532 | const out = makeOutput(opts.output, deps); |
| 3533 | const client = makeClient(opts, deps); |
| 3534 | const stderrFn = deps.stderr ?? ((line: string) => process.stderr.write(`${line}\n`)); |
| 3535 | |
| 3536 | // When --run-id is supplied, use the authoritative run-scoped endpoint |
| 3537 | // `GET /runs/{runId}?includeSteps=true` instead of client-filtering the |
| 3538 | // cumulative `/tests/{id}/steps` response. The cumulative FE Portal step |
| 3539 | // rows don't reliably carry per-run `runIdIfAvailable`, so the old |
| 3540 | // client-side filter always returned empty — even for completed runs. |
| 3541 | if (opts.runId !== undefined) { |
| 3542 | // 404 → NOT_FOUND (exit 4) — unknown or cross-tenant runId. |
| 3543 | // All other errors propagate unchanged (auth, transport, etc.). |
| 3544 | const run = await client.getRun(opts.runId, { includeSteps: true }); |
| 3545 | |
| 3546 | // Restore the implicit test-scoping the old `/tests/{testId}/steps` path |
| 3547 | // had: a runId belonging to a DIFFERENT test (same tenant) must not leak |
| 3548 | // that other test's steps under `test steps <thisTestId> --run-id <id>`. |
| 3549 | // Skipped under --dry-run — the canned sample's testId is fixed, not the |
| 3550 | // caller's argument, so a real comparison would always (falsely) mismatch. |
| 3551 | if (!opts.dryRun && run.testId !== opts.testId) { |
| 3552 | throw ApiError.fromEnvelope( |
| 3553 | { |
| 3554 | code: 'NOT_FOUND', |
| 3555 | message: `Run ${opts.runId} does not belong to test ${opts.testId} (it belongs to ${run.testId}). Use 'testsprite test steps ${run.testId} --run-id ${opts.runId}' or drop --run-id.`, |
| 3556 | }, |
| 3557 | 404, |
| 3558 | ); |
| 3559 | } |
| 3560 | |
| 3561 | const rawSteps = run.steps ?? []; |
| 3562 | const items: CliTestStep[] = rawSteps.map(s => mapRunStepToCliTestStep(s, run)); |
| 3563 | const page: Page<CliTestStep> = { items, nextToken: null }; |
| 3564 | |
| 3565 | if (items.length === 0) { |
| 3566 | // The run exists but has no recorded step rows yet (e.g. a fast |
| 3567 | // code-replay run that finished before any steps were written). |
| 3568 | if (opts.output === 'json') { |
| 3569 | out.print(page, data => renderStepsText(data as Page<CliTestStep>)); |
| 3570 | } else { |
| 3571 | stderrFn( |
| 3572 | `[advisory] No step records found for run ${opts.runId}. ` + |
| 3573 | `The run may have completed before steps were written, or this run type does not record per-step data. ` + |
| 3574 | `For the full failure bundle use: testsprite test artifact get ${opts.runId}`, |
| 3575 | ); |
| 3576 | } |
| 3577 | return page; |
| 3578 | } |
| 3579 | |
| 3580 | out.print(page, data => renderStepsText(data as Page<CliTestStep>)); |
| 3581 | return page; |
| 3582 | } |
| 3583 | |
| 3584 | // Bare `test steps <id>` (no --run-id): cumulative path — byte-identical to |
| 3585 | // the original behavior. Pagination flags are honored here. |
no test coverage detected