@testdriverai/agent 7.8.0-test.74 → 7.9.0-test.1
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 +156 -0
- package/package.json +2 -2
package/agent/lib/sandbox.js
CHANGED
|
@@ -536,6 +536,162 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
|
|
|
536
536
|
}
|
|
537
537
|
}
|
|
538
538
|
|
|
539
|
+
// ─── Handle pending slot claim (trigger.dev waitpoint flow) ────
|
|
540
|
+
// The API returned early with status: 'pending'. The SDK has now
|
|
541
|
+
// connected to Ably and entered presence (done in _initAbly above).
|
|
542
|
+
// Wait for the claim-slot task to publish slot-approved or slot-denied
|
|
543
|
+
// on the control channel, then re-call authenticate with slotApproved.
|
|
544
|
+
// On slot-denied, we poll forever (re-calling authenticate every 10s)
|
|
545
|
+
// until a slot opens, matching _httpPostWithConcurrencyRetry behavior.
|
|
546
|
+
var concurrencyRetryInterval = 10000;
|
|
547
|
+
var slotPollStart = Date.now();
|
|
548
|
+
while (reply.status === 'pending') {
|
|
549
|
+
logger.log('Slot claim pending — waiting for approval via Ably...');
|
|
550
|
+
|
|
551
|
+
var self = this;
|
|
552
|
+
var slotResolved = false;
|
|
553
|
+
var slotResolve, slotReject;
|
|
554
|
+
var slotDecisionPromise = new Promise(function (resolve, reject) {
|
|
555
|
+
slotResolve = resolve;
|
|
556
|
+
slotReject = reject;
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
var slotTimeout = setTimeout(function () {
|
|
560
|
+
if (slotResolved) return;
|
|
561
|
+
slotResolved = true;
|
|
562
|
+
try { self._sessionChannel.unsubscribe('control', onSlotControl); } catch (_) {}
|
|
563
|
+
slotReject(new Error('Slot claim timed out waiting for approval'));
|
|
564
|
+
}, 60000); // 60s timeout
|
|
565
|
+
if (slotTimeout.unref) slotTimeout.unref();
|
|
566
|
+
|
|
567
|
+
function onSlotControl(msg) {
|
|
568
|
+
var data = msg.data;
|
|
569
|
+
if (!data) return;
|
|
570
|
+
if (data.type === 'slot-approved') {
|
|
571
|
+
if (slotResolved) return;
|
|
572
|
+
slotResolved = true;
|
|
573
|
+
clearTimeout(slotTimeout);
|
|
574
|
+
try { self._sessionChannel.unsubscribe('control', onSlotControl); } catch (_) {}
|
|
575
|
+
slotResolve({ approved: true, data: data });
|
|
576
|
+
} else if (data.type === 'slot-denied') {
|
|
577
|
+
if (slotResolved) return;
|
|
578
|
+
slotResolved = true;
|
|
579
|
+
clearTimeout(slotTimeout);
|
|
580
|
+
try { self._sessionChannel.unsubscribe('control', onSlotControl); } catch (_) {}
|
|
581
|
+
slotResolve({ approved: false, data: data });
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Subscribe FIRST, then check history to close the race window
|
|
586
|
+
// between presence enter (done in _initAbly) and this subscription.
|
|
587
|
+
// The claim-slot task fires in response to presence enter, so the
|
|
588
|
+
// decision may already be published by the time we get here.
|
|
589
|
+
var slotControlSub = await self._sessionChannel.subscribe('control', onSlotControl);
|
|
590
|
+
|
|
591
|
+
// Check for decisions published before this subscription was active
|
|
592
|
+
if (!slotResolved && slotControlSub) {
|
|
593
|
+
try {
|
|
594
|
+
var histPage = await slotControlSub.historyBeforeSubscribe({ limit: 10 });
|
|
595
|
+
while (histPage && !slotResolved) {
|
|
596
|
+
for (var hi = 0; hi < histPage.items.length; hi++) {
|
|
597
|
+
onSlotControl(histPage.items[hi]);
|
|
598
|
+
if (slotResolved) break;
|
|
599
|
+
}
|
|
600
|
+
histPage = (!slotResolved && histPage.hasNext()) ? await histPage.next() : null;
|
|
601
|
+
}
|
|
602
|
+
} catch (histErr) {
|
|
603
|
+
logger.warn('[slots] Failed to check history for slot decision: ' + (histErr.message || histErr));
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
var slotDecision = await slotDecisionPromise;
|
|
608
|
+
|
|
609
|
+
if (!slotDecision.approved) {
|
|
610
|
+
// Slot denied — disconnect Ably and re-try the full authenticate
|
|
611
|
+
// flow after a delay, polling forever until a slot opens.
|
|
612
|
+
var elapsed = Date.now() - slotPollStart;
|
|
613
|
+
logger.log(
|
|
614
|
+
'Slot denied: ' + (slotDecision.data.message || 'concurrency limit reached') +
|
|
615
|
+
' — waiting ' + (concurrencyRetryInterval / 1000) + 's before retrying' +
|
|
616
|
+
' (' + Math.round(elapsed / 1000) + 's elapsed)...'
|
|
617
|
+
);
|
|
618
|
+
logger.log('Upgrade for more slots → https://console.testdriver.ai/checkout/team');
|
|
619
|
+
try {
|
|
620
|
+
if (this._ably) this._ably.close();
|
|
621
|
+
this._ably = null;
|
|
622
|
+
this._sessionChannel = null;
|
|
623
|
+
} catch (_) {}
|
|
624
|
+
|
|
625
|
+
await new Promise(function (resolve) {
|
|
626
|
+
var t = setTimeout(resolve, concurrencyRetryInterval);
|
|
627
|
+
if (t.unref) t.unref();
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// Re-call authenticate — this goes through _httpPostWithConcurrencyRetry
|
|
631
|
+
// so transient HTTP errors are also handled. The new reply will either
|
|
632
|
+
// be 'pending' again (loop continues) or succeed directly.
|
|
633
|
+
reply = await this._httpPostWithConcurrencyRetry(
|
|
634
|
+
"/api/v7/sandbox/authenticate",
|
|
635
|
+
body,
|
|
636
|
+
timeout,
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
if (!reply.success && reply.status !== 'pending') {
|
|
640
|
+
var retryErr = new Error(
|
|
641
|
+
reply.errorMessage || "Failed to allocate sandbox",
|
|
642
|
+
);
|
|
643
|
+
retryErr.responseData = reply;
|
|
644
|
+
throw retryErr;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Re-init Ably if we got a new pending reply with fresh credentials
|
|
648
|
+
if (reply.status === 'pending' && reply.ably && reply.ably.token) {
|
|
649
|
+
this._sandboxId = reply.sandboxId;
|
|
650
|
+
this._teamId = reply.teamId;
|
|
651
|
+
await this._initAbly(reply.ably.token, reply.ably.channel);
|
|
652
|
+
this.instanceSocketConnected = true;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
continue; // loop back to wait for the next slot decision
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
logger.log('Slot approved — provisioning sandbox...');
|
|
659
|
+
|
|
660
|
+
// Re-call authenticate with slotApproved flag to trigger provisioning
|
|
661
|
+
// Keep the same sandboxId so the Ably channel stays valid
|
|
662
|
+
var provisionBody = {
|
|
663
|
+
apiKey: this.apiKey,
|
|
664
|
+
version: version,
|
|
665
|
+
os: message.os || this.os || 'linux',
|
|
666
|
+
session: sessionId,
|
|
667
|
+
apiRoot: this.apiRoot,
|
|
668
|
+
sandboxId: this._sandboxId,
|
|
669
|
+
slotApproved: true,
|
|
670
|
+
};
|
|
671
|
+
if (message.resolution) provisionBody.resolution = message.resolution;
|
|
672
|
+
if (message.ci) provisionBody.ci = message.ci;
|
|
673
|
+
if (message.ami) provisionBody.ami = message.ami;
|
|
674
|
+
if (message.instanceType) provisionBody.instanceType = message.instanceType;
|
|
675
|
+
if (message.e2bTemplateId) provisionBody.e2bTemplateId = message.e2bTemplateId;
|
|
676
|
+
if (message.keepAlive !== undefined) provisionBody.keepAlive = message.keepAlive;
|
|
677
|
+
|
|
678
|
+
reply = await this._httpPostWithConcurrencyRetry(
|
|
679
|
+
"/api/v7/sandbox/authenticate",
|
|
680
|
+
provisionBody,
|
|
681
|
+
timeout,
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
if (!reply.success) {
|
|
685
|
+
var provisionErr = new Error(
|
|
686
|
+
reply.errorMessage || "Failed to provision sandbox after approval",
|
|
687
|
+
);
|
|
688
|
+
provisionErr.responseData = reply;
|
|
689
|
+
throw provisionErr;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
break; // slot approved and provisioned — exit the while loop
|
|
693
|
+
}
|
|
694
|
+
|
|
539
695
|
if (message.type === "create") {
|
|
540
696
|
// E2B (Linux) sandboxes return a url directly.
|
|
541
697
|
// We still need to wait for runner.ready since sandbox-agent.js runs inside E2B.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testdriverai/agent",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.9.0-test.1",
|
|
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",
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
},
|
|
117
117
|
"overrides": {
|
|
118
118
|
"glob": "^11.0.1",
|
|
119
|
-
"obug": "2.1.
|
|
119
|
+
"obug": "2.1.1",
|
|
120
120
|
"rimraf": "^5.0.10"
|
|
121
121
|
},
|
|
122
122
|
"peerDependencies": {
|