@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.
- package/agent/lib/sandbox.js +40 -0
- package/docs/v7/ci-cd.mdx +41 -0
- package/package.json +1 -1
package/agent/lib/sandbox.js
CHANGED
|
@@ -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):
|