release: v0.4.0#259
Conversation
Private-Snapshot-RevId: afa3bf501545df536cfa638a86ae096fb7c34880
WalkthroughThe release updates the CLI to 0.4.0 with project deletion, run cancellation, graceful signal detachment, backend metadata and artifact rendering, minimum-version handling, V3 routing visibility, Windows CI support, secret scanning, and expanded release documentation. ChangesCLI behavior and runtime
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install timed out. The project may have too many dependencies for the sandbox. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Test Coverage Report
|
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/commands/test.ts (1)
1400-1428: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftSupport clearing backend dependency declarations.
The update path treats empty arrays as “not supplied,” so users cannot remove existing declarations.
src/commands/test.ts#L1400-L1428: add explicit clear semantics and sendproduces: []/consumes: []when requested.src/commands/test.test.ts#L6359-L6379: replace the omission assertion with coverage for clearing existing declarations.As per path instructions, backend dependency declarations must be editable via
test update.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/commands/test.ts` around lines 1400 - 1428, Update the test update option handling in src/commands/test.ts lines 1400-1428 to distinguish omitted dependency arrays from explicitly provided empty arrays, allowing produces: [] and consumes: [] to be sent for clearing; preserve validation requiring at least one supplied field and map opts.needs to consumes. In src/commands/test.test.ts lines 6359-6379, replace the omission assertion with coverage verifying existing backend dependency declarations are cleared by sending empty produces and consumes arrays.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/commands/doctor.ts`:
- Around line 123-125: Update the V3-enabled branch in the doctor command to
call emitV3RoutingAdvisory only when output mode is text, suppressing it for
JSON output. Add a regression test covering JSON output with V3 enabled and
retain the advisory for text mode.
In `@src/commands/test.run.spec.ts`:
- Line 3895: Replace the 5 ms setTimeout in the polling test with a startup
synchronization promise resolved when the polling GET/fetch begins; await that
promise before calling shutdown.interrupt('SIGINT'), ensuring the abort listener
is installed and removing the real-timer dependency.
In `@src/commands/test.ts`:
- Around line 5992-6033: Update the multi-run cancellation flow around the row
error handling, `summary.errors`, and exit-precedence checks to retain each
typed error’s documented exit code instead of always throwing `CLIError` with
code 1. Store the relevant exit code with each error row, then select and apply
a deterministic precedence when multiple failures occur, while preserving the
existing notFound precedence and error reporting.
- Around line 10391-10409: The appendArtifactTail function currently limits
output by lines but can emit an oversized single line. Add a byte/character cap
to the artifact value before splitting, preserving the tail-oriented behavior
and ensuring text output remains bounded; update the reported byte count and
omitted-content indication to reflect the bounded output while retaining the
full-content reference for JSON.
In `@src/lib/poll.spec.ts`:
- Around line 852-853: Replace elapsed-time waits with mock-readiness promises
before calling shutdown.interrupt. In src/lib/poll.spec.ts at 852-853, signal
when neverSleep is entered; in src/lib/http.test.ts at 853-854 and 878-879,
signal when each retry sleep begins; in src/commands/test.wait.spec.ts at
1356-1359 and 1397-1398, signal when hangingFetch receives the request and reuse
that barrier; in src/commands/test.quickwins.spec.ts at 1029-1032 and
src/commands/test.rerun.spec.ts at 4959-4963, await barriers confirming both
polls are in flight before interrupting.
In `@test/e2e/signal.e2e.test.ts`:
- Line 107: Remove the hardcoded setTimeout delay in the signal-handling test
after await arrived. Use the existing arrived synchronization as the readiness
guarantee before continuing, keeping the test deterministic and free of real
timers.
---
Outside diff comments:
In `@src/commands/test.ts`:
- Around line 1400-1428: Update the test update option handling in
src/commands/test.ts lines 1400-1428 to distinguish omitted dependency arrays
from explicitly provided empty arrays, allowing produces: [] and consumes: [] to
be sent for clearing; preserve validation requiring at least one supplied field
and map opts.needs to consumes. In src/commands/test.test.ts lines 6359-6379,
replace the omission assertion with coverage verifying existing backend
dependency declarations are cleared by sending empty produces and consumes
arrays.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 3509dc37-fbca-4396-b1da-be6775721dee
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.json,!package-lock.jsontest/__snapshots__/help.snapshot.test.ts.snapis excluded by!**/*.snap,!**/*.snap
📒 Files selected for processing (56)
.github/workflows/backport-dispatch.yml.github/workflows/ci-nudge.yml.github/workflows/ci.yml.github/workflows/issue-triage.yml.github/workflows/pr-triage.yml.github/workflows/release.yaml.github/workflows/test-coverage.yml.gitleaks.tomlCHANGELOG.mdCONTRIBUTING.mdDOCUMENTATION.mdREADME.mdpackage.jsonscripts/README.mdskills/testsprite-verify.codex.mdskills/testsprite-verify.skill.mdsrc/commands/agent.test.tssrc/commands/auth.test.tssrc/commands/auth.tssrc/commands/doctor.test.tssrc/commands/doctor.tssrc/commands/project.test.tssrc/commands/project.tssrc/commands/test.cancel.spec.tssrc/commands/test.quickwins.spec.tssrc/commands/test.rerun.spec.tssrc/commands/test.run.spec.tssrc/commands/test.test.tssrc/commands/test.tssrc/commands/test.wait.spec.tssrc/index.tssrc/lib/bundle.test.tssrc/lib/bundle.tssrc/lib/client-factory.tssrc/lib/dry-run/samples.test.tssrc/lib/dry-run/samples.tssrc/lib/errors.test.tssrc/lib/errors.tssrc/lib/failing-fe-resolver.spec.tssrc/lib/failing-fe-resolver.tssrc/lib/http.test.tssrc/lib/http.tssrc/lib/interrupt.test.tssrc/lib/interrupt.tssrc/lib/poll.spec.tssrc/lib/poll.tssrc/lib/runs.types.tssrc/lib/v3-advisory.test.tssrc/lib/v3-advisory.tssrc/lib/version-notice.test.tssrc/lib/version-notice.tssrc/version.tstest/cli.subprocess.test.tstest/e2e/signal.e2e.test.tstest/help.snapshot.test.tstest/helpers/execNpm.ts
| if (connectivity.v3Enabled === true) { | ||
| emitV3RoutingAdvisory(stderr); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Suppress the V3 advisory in JSON mode.
doctor --output json currently emits plain-text advisory lines to stderr, unlike auth status. Gate this call on text mode and add a JSON/V3 regression test.
Proposed fix
- if (connectivity.v3Enabled === true) {
+ if (opts.output !== 'json' && connectivity.v3Enabled === true) {
emitV3RoutingAdvisory(stderr);
}As per path instructions, the V3 routing advisory must be emitted “in the V3-enabled case, text-mode only.”
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (connectivity.v3Enabled === true) { | |
| emitV3RoutingAdvisory(stderr); | |
| } | |
| if (opts.output !== 'json' && connectivity.v3Enabled === true) { | |
| emitV3RoutingAdvisory(stderr); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/commands/doctor.ts` around lines 123 - 125, Update the V3-enabled branch
in the doctor command to call emitV3RoutingAdvisory only when output mode is
text, suppressing it for JSON output. Add a regression test covering JSON output
with V3 enabled and retain the advisory for text mode.
Source: Path instructions
| shutdown, | ||
| }, | ||
| ); | ||
| setTimeout(() => shutdown.interrupt('SIGINT'), 5); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Synchronize on poll startup instead of sleeping for 5 ms.
The real timer can fire before the GET installs its abort listener, making this test timing-dependent. Resolve a promise when the polling fetch starts, await it, then call shutdown.interrupt('SIGINT').
As per path instructions, tests must be deterministic and use no real timers.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/commands/test.run.spec.ts` at line 3895, Replace the 5 ms setTimeout in
the polling test with a startup synchronization promise resolved when the
polling GET/fetch begins; await that promise before calling
shutdown.interrupt('SIGINT'), ensuring the abort listener is installed and
removing the real-timer dependency.
Source: Path instructions
| const message = err instanceof Error ? err.message : String(err); | ||
| rows.push({ runId, status: 'error', error: message }); | ||
| } | ||
| } | ||
|
|
||
| const errorRows = rows.filter(r => r.status === 'error'); | ||
| const summary: CliCancelSummary = { | ||
| cancelled: rows.filter(r => r.status === 'cancelled').map(r => r.runId), | ||
| alreadyCancelled: rows.filter(r => r.status === 'alreadyCancelled').map(r => r.runId), | ||
| conflicts: rows | ||
| .filter(r => r.status === 'conflict') | ||
| .map(r => ({ runId: r.runId, status: r.conflictStatus ?? 'unknown' })), | ||
| notFound: rows.filter(r => r.status === 'notFound').map(r => r.runId), | ||
| errors: errorRows.map(r => ({ runId: r.runId, message: r.error ?? 'unknown error' })), | ||
| }; | ||
|
|
||
| out.print(summary, data => renderCancelSummaryText(data as CliCancelSummary)); | ||
|
|
||
| const parts = [ | ||
| `${summary.cancelled.length} cancelled`, | ||
| `${summary.alreadyCancelled.length} already cancelled`, | ||
| ]; | ||
| if (summary.conflicts.length > 0) parts.push(`${summary.conflicts.length} conflict`); | ||
| if (summary.notFound.length > 0) parts.push(`${summary.notFound.length} not found`); | ||
| if (errorRows.length > 0) parts.push(`${errorRows.length} error`); | ||
| stderrFn(`Cancel summary: ${parts.join(', ')}.`); | ||
|
|
||
| // Exit precedence (CXL-11): notFound outranks conflict — a caller-side bug | ||
| // (wrong id / wrong tenant) is more actionable to surface than "it already | ||
| // finished". A bare transport/auth error on any member also fails loudly | ||
| // rather than being silently absorbed into a 0 exit. | ||
| if (summary.notFound.length > 0) { | ||
| throw new CLIError( | ||
| `${summary.notFound.length} run id${summary.notFound.length !== 1 ? 's' : ''} not found: ${summary.notFound.join(' ')}`, | ||
| 4, | ||
| ); | ||
| } | ||
| if (errorRows.length > 0) { | ||
| throw new CLIError( | ||
| `${errorRows.length} cancel request${errorRows.length !== 1 ? 's' : ''} failed: ${errorRows.map(r => r.runId).join(' ')}`, | ||
| 1, | ||
| ); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Preserve documented exit codes for multi-run cancellation failures.
Auth, transport, HTTP 426, and other typed errors are flattened into exit code 1. Retain each error’s documented exit code and define deterministic precedence rather than introducing this ad-hoc code.
As per path instructions, error paths must map to the documented exit code and must not introduce ad-hoc codes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/commands/test.ts` around lines 5992 - 6033, Update the multi-run
cancellation flow around the row error handling, `summary.errors`, and
exit-precedence checks to retain each typed error’s documented exit code instead
of always throwing `CLIError` with code 1. Store the relevant exit code with
each error row, then select and apply a deterministic precedence when multiple
failures occur, while preserving the existing notFound precedence and error
reporting.
Source: Path instructions
| const BACKEND_ARTIFACT_TAIL_LINES = 20; | ||
|
|
||
| function appendArtifactTail( | ||
| lines: string[], | ||
| label: string, | ||
| value: string | null | undefined, | ||
| ): void { | ||
| if (value == null || value === '') return; | ||
| const allLines = value.replace(/\n+$/, '').split('\n'); | ||
| const dropped = Math.max(0, allLines.length - BACKEND_ARTIFACT_TAIL_LINES); | ||
| const tail = dropped > 0 ? allLines.slice(-BACKEND_ARTIFACT_TAIL_LINES) : allLines; | ||
| const bytes = Buffer.byteLength(value, 'utf8'); | ||
| lines.push(''); | ||
| lines.push( | ||
| `${label} (${bytes} bytes${dropped > 0 ? `, showing last ${tail.length} lines` : ''}):`, | ||
| ); | ||
| for (const l of tail) lines.push(` ${l}`); | ||
| if (dropped > 0) | ||
| lines.push(` … ${dropped} earlier line(s) omitted — full content in --output json`); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Bound artifact tails by bytes as well as lines.
A single 50-KB line bypasses the 20-line limit and is printed in full. Apply a byte/character tail cap before splitting into lines.
Proposed direction
const BACKEND_ARTIFACT_TAIL_LINES = 20;
+const BACKEND_ARTIFACT_TAIL_BYTES = 8 * 1024;
- const allLines = value.replace(/\n+$/, '').split('\n');
+ const valueBuffer = Buffer.from(value, 'utf8');
+ const bounded =
+ valueBuffer.length > BACKEND_ARTIFACT_TAIL_BYTES
+ ? valueBuffer.subarray(-BACKEND_ARTIFACT_TAIL_BYTES).toString('utf8')
+ : value;
+ const allLines = bounded.replace(/\n+$/, '').split('\n');As per path instructions, text output must print a bounded tail with byte counts.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const BACKEND_ARTIFACT_TAIL_LINES = 20; | |
| function appendArtifactTail( | |
| lines: string[], | |
| label: string, | |
| value: string | null | undefined, | |
| ): void { | |
| if (value == null || value === '') return; | |
| const allLines = value.replace(/\n+$/, '').split('\n'); | |
| const dropped = Math.max(0, allLines.length - BACKEND_ARTIFACT_TAIL_LINES); | |
| const tail = dropped > 0 ? allLines.slice(-BACKEND_ARTIFACT_TAIL_LINES) : allLines; | |
| const bytes = Buffer.byteLength(value, 'utf8'); | |
| lines.push(''); | |
| lines.push( | |
| `${label} (${bytes} bytes${dropped > 0 ? `, showing last ${tail.length} lines` : ''}):`, | |
| ); | |
| for (const l of tail) lines.push(` ${l}`); | |
| if (dropped > 0) | |
| lines.push(` … ${dropped} earlier line(s) omitted — full content in --output json`); | |
| const BACKEND_ARTIFACT_TAIL_LINES = 20; | |
| const BACKEND_ARTIFACT_TAIL_BYTES = 8 * 1024; | |
| function appendArtifactTail( | |
| lines: string[], | |
| label: string, | |
| value: string | null | undefined, | |
| ): void { | |
| if (value == null || value === '') return; | |
| const valueBuffer = Buffer.from(value, 'utf8'); | |
| const bounded = | |
| valueBuffer.length > BACKEND_ARTIFACT_TAIL_BYTES | |
| ? valueBuffer.subarray(-BACKEND_ARTIFACT_TAIL_BYTES).toString('utf8') | |
| : value; | |
| const allLines = bounded.replace(/\n+$/, '').split('\n'); | |
| const dropped = Math.max(0, allLines.length - BACKEND_ARTIFACT_TAIL_LINES); | |
| const tail = dropped > 0 ? allLines.slice(-BACKEND_ARTIFACT_TAIL_LINES) : allLines; | |
| const bytes = Buffer.byteLength(value, 'utf8'); | |
| lines.push(''); | |
| lines.push( | |
| `${label} (${bytes} bytes${dropped > 0 ? `, showing last ${tail.length} lines` : ''}):`, | |
| ); | |
| for (const l of tail) lines.push(` ${l}`); | |
| if (dropped > 0) | |
| lines.push(` … ${dropped} earlier line(s) omitted — full content in --output json`); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/commands/test.ts` around lines 10391 - 10409, The appendArtifactTail
function currently limits output by lines but can emit an oversized single line.
Add a byte/character cap to the artifact value before splitting, preserving the
tail-oriented behavior and ensuring text output remains bounded; update the
reported byte count and omitted-content indication to reflect the bounded output
while retaining the full-content reference for JSON.
Source: Path instructions
| await new Promise(resolve => setTimeout(resolve, 10)); // let the loop reach the sleep | ||
| shutdown.interrupt('SIGTERM'); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Synchronize on mock readiness instead of elapsed time.
Fixed 5–25 ms delays can interrupt before the mocked fetch/sleep is reached on slow CI. Resolve a promise from the mock when it starts, await that barrier, then call shutdown.interrupt(...).
src/lib/poll.spec.ts#L852-L853: signal whenneverSleepis entered.src/lib/http.test.ts#L853-L854: signal when retry sleep starts.src/lib/http.test.ts#L878-L879: signal whenRetry-Aftersleep starts.src/commands/test.wait.spec.ts#L1356-L1359: signal whenhangingFetchreceives the request.src/commands/test.wait.spec.ts#L1397-L1398: use the same fetch-start barrier.src/commands/test.quickwins.spec.ts#L1029-L1032: wait until both run polls are in flight.src/commands/test.rerun.spec.ts#L4959-L4963: wait until both accepted-run polls are in flight.
📍 Affects 5 files
src/lib/poll.spec.ts#L852-L853(this comment)src/lib/http.test.ts#L853-L854src/lib/http.test.ts#L878-L879src/commands/test.wait.spec.ts#L1356-L1359src/commands/test.wait.spec.ts#L1397-L1398src/commands/test.quickwins.spec.ts#L1029-L1032src/commands/test.rerun.spec.ts#L4959-L4963
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/poll.spec.ts` around lines 852 - 853, Replace elapsed-time waits with
mock-readiness promises before calling shutdown.interrupt. In
src/lib/poll.spec.ts at 852-853, signal when neverSleep is entered; in
src/lib/http.test.ts at 853-854 and 878-879, signal when each retry sleep
begins; in src/commands/test.wait.spec.ts at 1356-1359 and 1397-1398, signal
when hangingFetch receives the request and reuse that barrier; in
src/commands/test.quickwins.spec.ts at 1029-1032 and
src/commands/test.rerun.spec.ts at 4959-4963, await barriers confirming both
polls are in flight before interrupting.
| ); | ||
|
|
||
| await arrived; // the long-poll fetch is in flight — the armed window is open | ||
| await new Promise(r => setTimeout(r, 150)); // let the request settle into the poll loop |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Avoid real timers in tests.
As per path instructions, tests must be deterministic and offline with no real timers. Using a hardcoded setTimeout of 150ms to wait for the child process to settle can lead to flaky tests in slower CI environments.
Since the CLI must arm the signal handler before initiating the HTTP request, the fact that the request has arrived at the server (await arrived) already guarantees the handler is active. You can likely safely remove this sleep altogether.
♻️ Proposed fix
- await new Promise(r => setTimeout(r, 150)); // let the request settle into the poll loop📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await new Promise(r => setTimeout(r, 150)); // let the request settle into the poll loop |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test/e2e/signal.e2e.test.ts` at line 107, Remove the hardcoded setTimeout
delay in the signal-handling test after await arrived. Use the existing arrived
synchronization as the readiness guarantee before continuing, keeping the test
deterministic and free of real timers.
Source: Path instructions
zeshi-du
left a comment
There was a problem hiding this comment.
Reviewed against the atlas snapshot source (release/public @ afa3bf5) — approving.
Integrity: PR head = the snapshot source minus exactly the sanctioned internal drops (dev-e2e tier, mock-backend internals, perf scripts, internal docs). The five deliberate divergences are all go-public transforms: internal-only npm scripts stripped, GitHub Packages restricted → public npm, :11443 dev endpoints scrubbed to prod URLs in contract/mock fixtures, and the reworked release.yaml (SHA-pinned actions, id-token: write provenance, exact npm@11.6.2 pin with the sigstore/npm-12.0.0 rationale documented — nice work, this is the DEV-321 root-cause fix).
Security: gitleaks green + manual pattern sweep (internal ports/hostnames/keys) zero hits. The new backport-dispatch.yml is a checkout-free pull_request_target reading event metadata only, contents: read, no-op until an operator arms the App secret — safe as written; keep the "do not add actions/checkout" invariant.
CI: every substantive check green incl. Windows + Node 22. The red gate check is the repo-wide broken issue-link gate (@actions/github module resolution — fails on ~16 open PRs regardless of content; community PR #257 fixes it) — not a content signal.
Heads-up, non-blocking: this cut does not include today's DEV-404 fix (agent install <target> positional silently installs the claude skill — atlas #280, in review), which the v0.4.0 validation flagged as fix-before-tag-recommended. It rides 0.4.1 unless we respin. Also note the first v* tag will be the first real exercise of the OIDC provenance path — watch the publish job.
TestSprite CLI v0.4.0. Full details in CHANGELOG.md.
Added
test cancel <run-id...>— cancel in-flight runs (Ctrl-C now detaches gracefully during--wait; cancel is the explicit stop)project delete <project-id> --confirm— cascade-delete a project and everything under ittest resultand failure bundlestest get) and editable (test update --produces/--needs/--category)auth status/doctorshow the account's platform routing when reported by the backendChanged / Fixed
--wait(exit 130/143/129, partial JSON envelope, re-attach hint)project create --descriptionnow fails fast with a clear error (projects have no description field)steps: n/a (backend)instead of a misleading0/0testsprite-verifyskill routes local-only changes to the TestSprite MCP instead of failing on localhostMaintainers: merge with a merge commit (no squash / no rebase).
Summary by CodeRabbit
test cancelfor cancelling one or multiple runs.project deletewith dry-run support.