@testdriverai/mcp 7.8.0-test.41 → 7.8.0-test.42

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.
@@ -98,7 +98,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
98
98
  }
99
99
 
100
100
  // Save subscription references for historyBeforeSubscribe() during discontinuity recovery
101
- this._responseSubscription = await this._sessionChannel.subscribe("response", function (msg) {
101
+ this._onResponseMsg = function (msg) {
102
102
  var message = msg.data;
103
103
  if (!message) return;
104
104
 
@@ -154,31 +154,53 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
154
154
  }
155
155
 
156
156
  if (!message.requestId || !self.ps[message.requestId]) {
157
- var debugMode =
158
- process.env.VERBOSE || process.env.TD_DEBUG;
159
- if (debugMode) {
160
- console.warn(
161
- "No pending promise found for requestId:",
162
- message.requestId,
163
- );
164
- }
157
+ var pendingIds = Object.keys(self.ps);
158
+ var pendingSummary = pendingIds.length > 0
159
+ ? pendingIds.map(function (rid) {
160
+ var e = self.ps[rid];
161
+ return rid + '(' + (e && e.message ? e.message.type : '?') + ')';
162
+ }).join(', ')
163
+ : 'none';
164
+ logger.warn(
165
+ '[ably] No pending promise for requestId=' + (message.requestId || 'null') +
166
+ ' | response type=' + (message.type || 'unknown') +
167
+ ' | error=' + (message.error ? (message.errorMessage || 'true') : 'false') +
168
+ ' | currently pending: [' + pendingSummary + ']'
169
+ );
165
170
  return;
166
171
  }
167
172
 
168
173
  if (message.error) {
169
- var pendingMessage =
170
- self.ps[message.requestId] &&
171
- self.ps[message.requestId].message;
174
+ var pendingEntry = self.ps[message.requestId];
175
+ var pendingMessage = pendingEntry && pendingEntry.message;
176
+ var pendingAge = pendingEntry && pendingEntry.startTime
177
+ ? ((Date.now() - pendingEntry.startTime) / 1000).toFixed(1) + 's'
178
+ : '?';
179
+ logger.warn(
180
+ '[ably] Promise REJECTED: requestId=' + message.requestId +
181
+ ' | type=' + (pendingMessage ? pendingMessage.type : 'unknown') +
182
+ ' | age=' + pendingAge +
183
+ ' | error=' + (message.errorMessage || 'Sandbox error')
184
+ );
172
185
  if (!pendingMessage || pendingMessage.type !== "output") {
173
186
  emitter.emit(events.error.sandbox, message.errorMessage);
174
187
  }
175
188
  var error = new Error(message.errorMessage || "Sandbox error");
176
189
  error.responseData = message;
177
190
  delete self._execBuffers[message.requestId];
178
- self.ps[message.requestId].reject(error);
191
+ pendingEntry.reject(error);
179
192
  } else {
180
193
  emitter.emit(events.sandbox.received);
181
194
  if (self.ps[message.requestId]) {
195
+ var resolveEntry = self.ps[message.requestId];
196
+ var resolveAge = resolveEntry.startTime
197
+ ? ((Date.now() - resolveEntry.startTime) / 1000).toFixed(1) + 's'
198
+ : '?';
199
+ logger.log(
200
+ '[ably] Promise RESOLVED: requestId=' + message.requestId +
201
+ ' | type=' + (resolveEntry.message ? resolveEntry.message.type : 'unknown') +
202
+ ' | age=' + resolveAge
203
+ );
182
204
  // Unwrap the result from the Ably response envelope
183
205
  // The runner sends { requestId, type, result, success }
184
206
  // But SDK commands expect just the result object
@@ -197,9 +219,10 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
197
219
  }
198
220
  }
199
221
  delete self.ps[message.requestId];
200
- });
222
+ };
223
+ this._responseSubscription = await this._sessionChannel.subscribe("response", this._onResponseMsg);
201
224
 
202
- this._fileSubscription = await this._sessionChannel.subscribe("file", function (msg) {
225
+ this._onFileMsg = function (msg) {
203
226
  var message = msg.data;
204
227
  if (!message) return;
205
228
  logger.log(`[ably] Received file: type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
@@ -209,7 +232,8 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
209
232
  delete self.ps[message.requestId];
210
233
  }
211
234
  emitter.emit(events.sandbox.file, message);
212
- });
235
+ };
236
+ this._fileSubscription = await this._sessionChannel.subscribe("file", this._onFileMsg);
213
237
 
214
238
  this.heartbeat = setInterval(function () { }, 5000);
215
239
  if (this.heartbeat.unref) this.heartbeat.unref();
@@ -218,8 +242,19 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
218
242
  this._statsInterval = setInterval(() => {
219
243
  const connState = this._ably ? this._ably.connection.state : 'no-client';
220
244
  const chState = this._sessionChannel ? this._sessionChannel.state : 'null';
221
- const pending = Object.keys(this.ps).length;
245
+ const pendingIds = Object.keys(this.ps);
246
+ const pending = pendingIds.length;
222
247
  logger.log(`[ably][stats] connection=${connState} | sandbox=${this._sandboxId} | pending=${pending} | channel=${chState}`);
248
+ if (pending > 0) {
249
+ const now = Date.now();
250
+ for (const rid of pendingIds) {
251
+ const entry = this.ps[rid];
252
+ if (!entry) continue;
253
+ const type = entry.message ? entry.message.type : 'unknown';
254
+ const ageSec = ((now - (entry.startTime || now)) / 1000).toFixed(1);
255
+ logger.log(`[ably][stats] pending: requestId=${rid} | type=${type} | age=${ageSec}s`);
256
+ }
257
+ }
223
258
  }, 10000);
224
259
  if (this._statsInterval.unref) this._statsInterval.unref();
225
260
 
@@ -267,12 +302,14 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
267
302
  /**
268
303
  * Recover missed messages after a channel discontinuity.
269
304
  * Uses historyBeforeSubscribe() on each subscription, which guarantees
270
- * no gap between historical and live messages.
305
+ * no gap between historical and live messages. Each recovered message
306
+ * is dispatched through the same handler that processes live messages
307
+ * so that pending promises are resolved/rejected correctly.
271
308
  */
272
309
  async _recoverFromDiscontinuity() {
273
310
  var subs = [
274
- { name: 'response', sub: this._responseSubscription },
275
- { name: 'file', sub: this._fileSubscription },
311
+ { name: 'response', sub: this._responseSubscription, handler: this._onResponseMsg },
312
+ { name: 'file', sub: this._fileSubscription, handler: this._onFileMsg },
276
313
  ];
277
314
  var totalRecovered = 0;
278
315
  for (var i = 0; i < subs.length; i++) {
@@ -283,17 +320,29 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
283
320
  var page = await entry.sub.historyBeforeSubscribe({ limit: 100 });
284
321
  var recovered = 0;
285
322
  while (page) {
286
- recovered += page.items.length;
323
+ // Replay each missed message through the handler so pending
324
+ // promises get resolved instead of timing out.
325
+ for (var j = 0; j < page.items.length; j++) {
326
+ recovered++;
327
+ try {
328
+ if (entry.handler) {
329
+ logger.log('[ably] Replaying recovered ' + entry.name + ' message (requestId=' + (page.items[j].data && page.items[j].data.requestId || 'none') + ')');
330
+ entry.handler(page.items[j]);
331
+ }
332
+ } catch (replayErr) {
333
+ logger.error('[ably] Error replaying recovered message: ' + (replayErr.message || replayErr));
334
+ }
335
+ }
287
336
  page = page.hasNext() ? await page.next() : null;
288
337
  }
289
338
  totalRecovered += recovered;
290
- logger.log('[ably] Discontinuity recovery: found ' + recovered + ' ' + entry.name + ' message(s) in gap');
339
+ logger.log('[ably] Discontinuity recovery: replayed ' + recovered + ' ' + entry.name + ' message(s) from gap');
291
340
  } catch (err) {
292
341
  logger.error('[ably] Discontinuity recovery failed for ' + entry.name + ': ' + (err.message || err));
293
342
  }
294
343
  }
295
344
  if (totalRecovered > 0) {
296
- logger.warn('[ably] Recovered ' + totalRecovered + ' message(s) that were missed during connection interruption');
345
+ logger.warn('[ably] Recovered and replayed ' + totalRecovered + ' message(s) that were missed during connection interruption');
297
346
  } else {
298
347
  logger.log('[ably] Discontinuity recovery: no missed messages found');
299
348
  }
@@ -782,6 +831,18 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
782
831
 
783
832
  var timeoutId = setTimeout(function () {
784
833
  if (self.ps[requestId]) {
834
+ var pendingIds = Object.keys(self.ps);
835
+ var pendingSummary = pendingIds.map(function (rid) {
836
+ var e = self.ps[rid];
837
+ var age = e && e.startTime ? ((Date.now() - e.startTime) / 1000).toFixed(1) + 's' : '?';
838
+ return rid + '(' + (e && e.message ? e.message.type : '?') + ', ' + age + ')';
839
+ }).join(', ');
840
+ logger.error(
841
+ '[ably] Promise TIMEOUT: requestId=' + requestId +
842
+ ' | type=' + message.type +
843
+ ' | timeout=' + timeout + 'ms' +
844
+ ' | all pending: [' + pendingSummary + ']'
845
+ );
785
846
  delete self.ps[requestId];
786
847
  delete self._execBuffers[requestId];
787
848
  rejectPromise(
@@ -42,14 +42,14 @@ function checkVitestVersion() {
42
42
  if (major < MINIMUM_VITEST_VERSION) {
43
43
  throw new Error(
44
44
  `TestDriver requires Vitest >= ${MINIMUM_VITEST_VERSION}.0.0, but found ${version}. ` +
45
- `Please upgrade Vitest: npm install vitest@latest`,
45
+ `Please upgrade Vitest: npm install vitest@latest`,
46
46
  );
47
47
  }
48
48
  } catch (err) {
49
49
  if (err.code === "MODULE_NOT_FOUND") {
50
50
  throw new Error(
51
51
  "TestDriver requires Vitest to be installed. " +
52
- "Please install it: npm install vitest@latest",
52
+ "Please install it: npm install vitest@latest",
53
53
  );
54
54
  }
55
55
  throw err;
@@ -646,7 +646,7 @@ export function TestDriver(context, options = {}) {
646
646
 
647
647
  // Wait for connection to finish if it was initiated
648
648
  if (currentInstance.__connectionPromise) {
649
- await currentInstance.__connectionPromise.catch(() => {}); // Ignore connection errors during cleanup
649
+ await currentInstance.__connectionPromise.catch(() => { }); // Ignore connection errors during cleanup
650
650
  }
651
651
 
652
652
  // Disconnect with timeout
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testdriverai/mcp",
3
- "version": "7.8.0-test.41",
3
+ "version": "7.8.0-test.42",
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",