Skip to content

Commit 53175e9

Browse files
ahndohunclaude
andcommitted
fix(test): emit auto-minted idempotency-key under --output json in rerun and run --all
test run, test create, create-batch, plan put, code put, update, and delete all print the auto-minted idempotency key to stderr under --output json (as well as --verbose / --debug) so JSON-mode automation can capture the key and replay a retry safely. test rerun and test run --all minted a key but only echoed it under --debug / --verbose, so CI flows using --output json silently lost it. Align both paths with the shared guard used by every other minting site, and cover the JSON-mode emission (and the text-mode silence) with regression tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 3ab8136 commit 53175e9

4 files changed

Lines changed: 105 additions & 10 deletions

File tree

DOCUMENTATION.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ testsprite test run test_xxxxxxxx --target-url https://staging.example.com \
346346
testsprite test run test_xxxxxxxx --dry-run --output json
347347
```
348348

349-
`--target-url` must be a publicly reachable URL — the CLI pre-flights it against local addresses (`localhost`, `127.x`, `::1`, `0.0.0.0`, `169.254.x`, RFC1918) and the backend resolves it via DNS. For testing against localhost, use the [TestSprite MCP plugin](https://www.testsprite.com/docs), which handles the local tunnel. The CLI auto-mints an idempotency key (printed to stderr at `--verbose`); pass `--idempotency-key <uuid>` to control it explicitly.
349+
`--target-url` must be a publicly reachable URL — the CLI pre-flights it against local addresses (`localhost`, `127.x`, `::1`, `0.0.0.0`, `169.254.x`, RFC1918) and the backend resolves it via DNS. For testing against localhost, use the [TestSprite MCP plugin](https://www.testsprite.com/docs), which handles the local tunnel. The CLI auto-mints an idempotency key (printed to stderr under `--output json`, `--verbose`, or `--debug`); pass `--idempotency-key <uuid>` to control it explicitly.
350350

351351
#### `testsprite test rerun [test-id...]`
352352

@@ -376,7 +376,7 @@ Flags:
376376
- `--auto-heal` / `--no-auto-heal` — frontend AI heal-on-drift, **on by default** for FE reruns; opt out with `--no-auto-heal`. Verbatim-replay passes are free; a heal engage costs a small amount of credit. Ignored for backend tests.
377377
- `--skip-dependencies` — backend only: rerun just the named test without expanding the producer/teardown closure.
378378
- `--max-concurrency <n>` — with `--wait`, cap on in-flight polls during a batch rerun.
379-
- `--idempotency-key <key>` — auto-minted when omitted.
379+
- `--idempotency-key <key>` — auto-minted when omitted (the minted key is printed to stderr under `--output json`, `--verbose`, or `--debug`).
380380

381381
A batch rerun returns `accepted[]` (one `runId` per dispatched test) plus `deferred[]` for any test shed by the per-key run-rate limit; under `--wait`, a non-empty `deferred[]` exits 7 with a `nextAction` you can retry with a fresh idempotency key.
382382

src/commands/test.rerun.spec.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1637,6 +1637,74 @@ describe('--idempotency-key passthrough', () => {
16371637

16381638
expect(receivedKey).toBe('my-custom-key-abc');
16391639
});
1640+
1641+
it('emits the auto-minted idempotency-key on stderr in JSON output mode (parity with test run)', async () => {
1642+
const creds = makeCreds();
1643+
const rerunResp = makeFeRerunResp();
1644+
const stderrLines: string[] = [];
1645+
1646+
const fetchImpl = makeFetch(url => {
1647+
if (url.includes('/tests/test_fe_01/runs/rerun')) {
1648+
return { body: rerunResp };
1649+
}
1650+
return errorBody('NOT_FOUND');
1651+
});
1652+
1653+
await runTestRerun(
1654+
{
1655+
testIds: ['test_fe_01'],
1656+
all: false,
1657+
wait: false,
1658+
timeoutSeconds: 600,
1659+
autoHeal: false,
1660+
autoHealExplicit: false,
1661+
skipDependencies: false,
1662+
maxConcurrency: 10,
1663+
output: 'json',
1664+
profile: 'default',
1665+
dryRun: false,
1666+
debug: false,
1667+
verbose: false,
1668+
},
1669+
{ ...creds, sleep: instantSleep, fetchImpl, stderr: line => stderrLines.push(line) },
1670+
);
1671+
1672+
expect(stderrLines.some(l => l.startsWith('idempotency-key:'))).toBe(true);
1673+
});
1674+
1675+
it('does NOT emit an idempotency-key line in default text mode', async () => {
1676+
const creds = makeCreds();
1677+
const rerunResp = makeFeRerunResp();
1678+
const stderrLines: string[] = [];
1679+
1680+
const fetchImpl = makeFetch(url => {
1681+
if (url.includes('/tests/test_fe_01/runs/rerun')) {
1682+
return { body: rerunResp };
1683+
}
1684+
return errorBody('NOT_FOUND');
1685+
});
1686+
1687+
await runTestRerun(
1688+
{
1689+
testIds: ['test_fe_01'],
1690+
all: false,
1691+
wait: false,
1692+
timeoutSeconds: 600,
1693+
autoHeal: false,
1694+
autoHealExplicit: false,
1695+
skipDependencies: false,
1696+
maxConcurrency: 10,
1697+
output: 'text',
1698+
profile: 'default',
1699+
dryRun: false,
1700+
debug: false,
1701+
verbose: false,
1702+
},
1703+
{ ...creds, sleep: instantSleep, fetchImpl, stderr: line => stderrLines.push(line) },
1704+
);
1705+
1706+
expect(stderrLines.some(l => l.includes('idempotency-key:'))).toBe(false);
1707+
});
16401708
});
16411709

16421710
// ---------------------------------------------------------------------------

src/commands/test.run.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3470,6 +3470,39 @@ describe('dashboardUrl on run completion', () => {
34703470
);
34713471
});
34723472

3473+
it('run --all: emits the auto-minted idempotency-key on stderr in JSON output mode (parity with test run)', async () => {
3474+
const { credentialsPath } = makeCreds('sk-user-test', PROD_API);
3475+
const batchResp: BatchRunFreshResponse = {
3476+
accepted: [
3477+
{ testId: 'test_be_01', runId: 'run_f_01', enqueuedAt: '2026-06-10T10:00:00.000Z' },
3478+
],
3479+
conflicts: [],
3480+
deferred: [],
3481+
skippedFrontend: [],
3482+
skippedIntegration: [],
3483+
};
3484+
const stderrLines: string[] = [];
3485+
await runTestRunAll(
3486+
{
3487+
profile: 'default',
3488+
output: 'json',
3489+
debug: false,
3490+
projectId: 'project_be',
3491+
wait: false,
3492+
timeoutSeconds: 600,
3493+
maxConcurrency: 10,
3494+
},
3495+
{
3496+
credentialsPath,
3497+
fetchImpl: makeFetch(() => ({ body: batchResp })),
3498+
stdout: () => undefined,
3499+
stderr: line => stderrLines.push(line),
3500+
sleep: instantSleep,
3501+
},
3502+
);
3503+
expect(stderrLines.some(l => l.startsWith('idempotency-key:'))).toBe(true);
3504+
});
3505+
34733506
it('run --all --wait (prod endpoint): summary items carry dashboardUrl + stderr Dashboard line', async () => {
34743507
const { credentialsPath } = makeCreds('sk-user-test', PROD_API);
34753508
const batchResp: BatchRunFreshResponse = {

src/commands/test.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5136,12 +5136,9 @@ export async function runTestRunAll(
51365136
};
51375137

51385138
const idempotencyKey = opts.idempotencyKey ?? `cli-batch-run-fresh-${randomUUID()}`;
5139-
if (opts.idempotencyKey === undefined && opts.debug) {
5139+
if (opts.idempotencyKey === undefined && (opts.output === 'json' || opts.verbose || opts.debug)) {
51405140
stderrFn(`idempotency-key: ${idempotencyKey}`);
51415141
}
5142-
if (opts.idempotencyKey === undefined && opts.verbose) {
5143-
stderrFn(`[verbose] auto-minted idempotency-key: ${idempotencyKey}`);
5144-
}
51455142

51465143
// Resolve testIds: fetch all BE tests in the project, apply --filter.
51475144
let testIds: string[] | undefined;
@@ -5704,12 +5701,9 @@ export async function runTestRerun(
57045701
// slow rerun trigger / long-poll under load isn't cut at the 120s default.
57055702
const client = makeClient({ ...opts, requestTimeoutMs: resolveWaitRequestTimeoutMs(opts) }, deps);
57065703
const idempotencyKey = opts.idempotencyKey ?? `cli-rerun-${randomUUID()}`;
5707-
if (opts.idempotencyKey === undefined && opts.debug) {
5704+
if (opts.idempotencyKey === undefined && (opts.output === 'json' || opts.verbose || opts.debug)) {
57085705
stderrFn(`idempotency-key: ${idempotencyKey}`);
57095706
}
5710-
if (opts.idempotencyKey === undefined && opts.verbose) {
5711-
stderrFn(`[verbose] auto-minted idempotency-key: ${idempotencyKey}`);
5712-
}
57135707

57145708
// -------------------------------------------------------------------------
57155709
// Single rerun path

0 commit comments

Comments
 (0)