@testdriverai/mcp 7.9.174-test → 7.9.176-test

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,17 @@ const { version } = require("../../package.json");
6
6
  const { withRetry, getSentryTraceHeaders } = require("./sdk");
7
7
  const sentry = require("../../lib/sentry");
8
8
 
9
+ // How long (ms) to keep polling for a free concurrency slot before giving up.
10
+ // Configurable via TD_CONCURRENCY_MAX_WAIT (in seconds). Defaults to 60s.
11
+ // A value of 0 (or negative) disables polling entirely — fail on first denial.
12
+ function resolveConcurrencyMaxWait() {
13
+ var raw = process.env.TD_CONCURRENCY_MAX_WAIT;
14
+ if (raw === undefined || raw === "") return 60000;
15
+ var seconds = Number(raw);
16
+ if (!Number.isFinite(seconds) || seconds < 0) return 60000;
17
+ return Math.round(seconds * 1000);
18
+ }
19
+
9
20
  const createSandbox = function (emitter, analytics, sessionInstance) {
10
21
  class Sandbox {
11
22
  constructor() {
@@ -463,6 +474,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
463
474
  */
464
475
  async _httpPostWithConcurrencyRetry(path, body, timeout) {
465
476
  var concurrencyRetryInterval = 10000; // 10 seconds between concurrency retries
477
+ var concurrencyMaxWait = resolveConcurrencyMaxWait(); // give up polling for a slot after this many ms
466
478
  var startTime = Date.now();
467
479
  var sessionId = this.sessionInstance ? this.sessionInstance.get() : null;
468
480
 
@@ -506,6 +518,15 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
506
518
  var responseData = err.response && err.response.data;
507
519
  if (responseData && responseData.errorCode === "CONCURRENCY_LIMIT_EXCEEDED") {
508
520
  var elapsed = Date.now() - startTime;
521
+ if (elapsed >= concurrencyMaxWait) {
522
+ var giveUpErr = new Error(
523
+ (responseData.errorMessage || responseData.message || "Concurrency limit reached") +
524
+ " — gave up after " + Math.round(elapsed / 1000) + "s " +
525
+ "(max wait " + (concurrencyMaxWait / 1000) + "s).",
526
+ );
527
+ giveUpErr.responseData = responseData;
528
+ throw giveUpErr;
529
+ }
509
530
  logger.log(
510
531
  "Concurrency limit reached — waiting " +
511
532
  concurrencyRetryInterval / 1000 +
@@ -614,6 +635,10 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
614
635
  // On slot-denied, we poll forever (re-calling authenticate every 10s)
615
636
  // until a slot opens, matching _httpPostWithConcurrencyRetry behavior.
616
637
  var concurrencyRetryInterval = 10000;
638
+ // Overall cap on how long we'll keep polling for a free slot before
639
+ // giving up. Keeps a queued agent from hanging forever when the account
640
+ // is at its concurrency limit. Configurable via TD_CONCURRENCY_MAX_WAIT.
641
+ var concurrencyMaxWait = resolveConcurrencyMaxWait();
617
642
  var slotPollStart = Date.now();
618
643
  while (reply.status === 'pending') {
619
644
  logger.log('Slot claim pending — waiting for approval via Ably...');
@@ -689,6 +714,21 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
689
714
  // Slot denied — disconnect Ably and re-try the full authenticate
690
715
  // flow after a delay, polling forever until a slot opens.
691
716
  var elapsed = Date.now() - slotPollStart;
717
+ if (elapsed >= concurrencyMaxWait) {
718
+ try {
719
+ if (this._ably) this._ably.close();
720
+ this._ably = null;
721
+ this._sessionChannel = null;
722
+ } catch (_) {}
723
+ var giveUpErr = new Error(
724
+ 'Slot denied: ' + (slotDecision.data.message || 'concurrency limit reached') +
725
+ ' — gave up after ' + Math.round(elapsed / 1000) + 's ' +
726
+ '(max wait ' + (concurrencyMaxWait / 1000) + 's). ' +
727
+ 'Upgrade for more slots → https://console.testdriver.ai/checkout/team'
728
+ );
729
+ giveUpErr.responseData = slotDecision.data;
730
+ throw giveUpErr;
731
+ }
692
732
  logger.log(
693
733
  'Slot denied: ' + (slotDecision.data.message || 'concurrency limit reached') +
694
734
  ' — waiting ' + (concurrencyRetryInterval / 1000) + 's before retrying' +
package/docs/v7/ci-cd.mdx CHANGED
@@ -635,6 +635,47 @@ describe("Cross-platform tests", () => {
635
635
  });
636
636
  ```
637
637
 
638
+ ## Concurrency limits
639
+
640
+ Your plan allows a fixed number of sandboxes running at once. When a test asks for
641
+ a sandbox and you're already at that limit, the request is **queued** rather than
642
+ failed immediately: the SDK waits for a slot to free up, retrying every 10 seconds,
643
+ then proceeds automatically once one opens. This is what lets a parallel CI matrix
644
+ (many jobs starting at once) work on a plan with fewer slots than jobs — the extra
645
+ jobs simply wait their turn instead of erroring.
646
+
647
+ By default the SDK waits up to **60 seconds** for a slot before giving up with a
648
+ concurrency-limit error. Control that ceiling with `TD_CONCURRENCY_MAX_WAIT`:
649
+
650
+ | Value | Behavior |
651
+ | ----- | -------- |
652
+ | _unset_ | Wait up to **60 seconds** (the default). |
653
+ | `TD_CONCURRENCY_MAX_WAIT=300` | Wait up to **300 seconds** (5 minutes) before giving up. |
654
+ | `TD_CONCURRENCY_MAX_WAIT=0` | **Don't queue** — fail on the first denial. |
655
+
656
+ The value is **in seconds** (fractional values are allowed and rounded to the
657
+ nearest millisecond). Any invalid or negative value falls back to the 60-second
658
+ default. The wait applies per sandbox request, across both the initial allocation
659
+ and the realtime slot-approval handshake.
660
+
661
+ ```yaml
662
+ # Example: a large parallel matrix that may queue for a while.
663
+ # Give each job up to 5 minutes to acquire a slot before failing.
664
+ - name: Run TestDriver tests
665
+ env:
666
+ TD_API_KEY: ${{ secrets.TD_API_KEY }}
667
+ TD_CONCURRENCY_MAX_WAIT: "300"
668
+ run: npx vitest run
669
+ ```
670
+
671
+ <Tip>
672
+ Raise `TD_CONCURRENCY_MAX_WAIT` when you run more parallel jobs than your plan has
673
+ slots and would rather they queue than fail. Set it to `0` when you'd prefer a job
674
+ to **fail fast** on a busy account (e.g. a quick smoke test that shouldn't sit
675
+ waiting). When jobs routinely give up waiting, that's the signal to
676
+ [add more slots](https://console.testdriver.ai/checkout/team).
677
+ </Tip>
678
+
638
679
  ## Viewing Results
639
680
 
640
681
  All test runs are automatically recorded and visible in your TestDriver dashboard at [console.testdriver.ai](https://console.testdriver.ai):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testdriverai/mcp",
3
- "version": "7.9.174-test",
3
+ "version": "7.9.176-test",
4
4
  "description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
5
5
  "main": "sdk.js",
6
6
  "types": "sdk.d.ts",