@testdriverai/mcp 7.8.0-test.40 → 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.
- package/agent/lib/sandbox.js +84 -23
- package/lib/vitest/hooks.mjs +3 -3
- package/package.json +1 -1
package/agent/lib/sandbox.js
CHANGED
|
@@ -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.
|
|
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
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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:
|
|
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(
|
package/lib/vitest/hooks.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|