testdriverai 7.3.28 → 7.3.29

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/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## [7.3.29](https://github.com/testdriverai/testdriverai/compare/v7.3.28...v7.3.29) (2026-02-20)
2
+
3
+
4
+
1
5
  ## [7.3.28](https://github.com/testdriverai/testdriverai/compare/v7.3.27...v7.3.28) (2026-02-20)
2
6
 
3
7
 
@@ -1299,18 +1299,30 @@ function calculateStatsFromModules(testModules) {
1299
1299
  let failedTests = 0;
1300
1300
  let skippedTests = 0;
1301
1301
 
1302
+ // Guard against corrupt or circular test tree structures
1303
+ // (can happen with --sequence.concurrent in some Vitest versions)
1304
+ const seen = new Set();
1305
+
1302
1306
  for (const testModule of testModules) {
1303
- for (const testCase of testModule.children.allTests()) {
1304
- const result = testCase.result();
1305
- if (result.state === "passed") {
1306
- passedTests++;
1307
- totalTests++;
1308
- } else if (result.state === "failed") {
1309
- failedTests++;
1310
- totalTests++;
1311
- } else if (result.state === "skipped") {
1312
- skippedTests++;
1307
+ try {
1308
+ for (const testCase of testModule.children.allTests()) {
1309
+ // Deduplicate - skip if we've already counted this test
1310
+ if (seen.has(testCase.id)) continue;
1311
+ seen.add(testCase.id);
1312
+
1313
+ const result = testCase.result();
1314
+ if (result.state === "passed") {
1315
+ passedTests++;
1316
+ totalTests++;
1317
+ } else if (result.state === "failed") {
1318
+ failedTests++;
1319
+ totalTests++;
1320
+ } else if (result.state === "skipped") {
1321
+ skippedTests++;
1322
+ }
1313
1323
  }
1324
+ } catch (err) {
1325
+ logger.warn(`Error calculating stats for module: ${err.message}`);
1314
1326
  }
1315
1327
  }
1316
1328
 
@@ -59,113 +59,132 @@ function checkVitestVersion() {
59
59
  checkVitestVersion();
60
60
 
61
61
  /**
62
- * Set up console spies using Vitest's vi.spyOn to intercept console logs
63
- * and forward them to the sandbox for Dashcam visibility.
64
- * This is test-isolated and doesn't cause conflicts with concurrent tests.
65
- * @param {TestDriver} client - TestDriver client instance
66
- * @param {string} taskId - Unique task identifier for this test
62
+ * Singleton console spy that forwards logs to all active sandbox connections.
63
+ *
64
+ * When --sequence.concurrent is used, multiple tests run at the same time in
65
+ * the same worker process. The previous implementation called vi.spyOn on
66
+ * console.log once per test, stacking N mock layers deep. Every console.log
67
+ * then cascaded through all N layers — each one calling JSON.stringify inside
68
+ * forwardToSandbox — easily exceeding the call-stack limit for ≥ ~30 tests.
69
+ *
70
+ * This singleton intercepts the console methods exactly **once** and keeps a
71
+ * Set of active sandbox clients. Each log call is forwarded to every active
72
+ * client's sandbox in O(N) *flat* iterations instead of O(N) nested frames.
67
73
  */
68
- function setupConsoleSpy(client, taskId) {
69
- // Debug logging for console spy setup
70
- const debugConsoleSpy = process.env.TD_DEBUG_CONSOLE_SPY === "true";
71
- if (debugConsoleSpy) {
72
- process.stdout.write(`[DEBUG setupConsoleSpy] taskId: ${taskId}\n`);
73
- process.stdout.write(
74
- `[DEBUG setupConsoleSpy] client.sandbox exists: ${!!client.sandbox}\n`,
75
- );
76
- process.stdout.write(
77
- `[DEBUG setupConsoleSpy] client.sandbox?.instanceSocketConnected: ${client.sandbox?.instanceSocketConnected}\n`,
78
- );
79
- process.stdout.write(
80
- `[DEBUG setupConsoleSpy] client.sandbox?.send: ${typeof client.sandbox?.send}\n`,
81
- );
82
- }
74
+ const _consoleSpy = {
75
+ /** @type {Set<import('../../sdk.js').default>} */
76
+ activeClients: new Set(),
77
+ installed: false,
78
+ /** Original (un-spied) console references, captured once. */
79
+ originals: /** @type {{ log: Function, error: Function, warn: Function, info: Function } | null} */ (null),
80
+ spies: /** @type {{ log: any, error: any, warn: any, info: any } | null} */ (null),
81
+ };
83
82
 
84
- // Track forwarding stats
85
- let forwardedCount = 0;
86
- let skippedCount = 0;
83
+ const debugConsoleSpy = process.env.TD_DEBUG_CONSOLE_SPY === "true";
87
84
 
88
- // Helper to forward logs to sandbox
89
- const forwardToSandbox = (args) => {
90
- const message = args
91
- .map((arg) =>
92
- typeof arg === "object" ? JSON.stringify(arg, null, 2) : String(arg),
93
- )
94
- .join(" ");
85
+ /**
86
+ * Serialise console args to a single string for sandbox forwarding.
87
+ * Falls back to toString on circular/huge objects to avoid blowing out the
88
+ * stack inside JSON.stringify.
89
+ */
90
+ function serialiseConsoleArgs(args) {
91
+ return args
92
+ .map((arg) => {
93
+ if (typeof arg === "object" && arg !== null) {
94
+ try {
95
+ return JSON.stringify(arg, null, 2);
96
+ } catch {
97
+ // Circular reference or too deep — fall back safely
98
+ return String(arg);
99
+ }
100
+ }
101
+ return String(arg);
102
+ })
103
+ .join(" ");
104
+ }
95
105
 
96
- // Send to sandbox for immediate visibility in dashcam
106
+ /**
107
+ * Forward a console message to every active sandbox client.
108
+ * Called from the (single) console spy.
109
+ */
110
+ function forwardToAllSandboxes(args) {
111
+ if (_consoleSpy.activeClients.size === 0) return;
112
+
113
+ const message = serialiseConsoleArgs(args);
114
+ const encoded = Buffer.from(message, "utf8").toString("base64");
115
+
116
+ for (const client of _consoleSpy.activeClients) {
97
117
  if (client.sandbox && client.sandbox.instanceSocketConnected) {
98
118
  try {
99
- client.sandbox.send({
100
- type: "output",
101
- output: Buffer.from(message, "utf8").toString("base64"),
102
- });
103
- forwardedCount++;
104
- if (debugConsoleSpy && forwardedCount <= 3) {
105
- process.stdout.write(
106
- `[DEBUG forwardToSandbox] Forwarded message #${forwardedCount}: "${message.substring(0, 50)}..."\n`,
107
- );
108
- }
109
- } catch (err) {
110
- if (debugConsoleSpy) {
111
- process.stdout.write(
112
- `[DEBUG forwardToSandbox] Error sending: ${err.message}\n`,
113
- );
114
- }
115
- }
116
- } else {
117
- skippedCount++;
118
- if (debugConsoleSpy && skippedCount <= 3) {
119
- process.stdout.write(
120
- `[DEBUG forwardToSandbox] SKIPPED (sandbox not connected): "${message.substring(0, 50)}..."\n`,
121
- );
119
+ client.sandbox.send({ type: "output", output: encoded });
120
+ } catch {
121
+ // fire-and-forget — don't let one broken socket block the others
122
122
  }
123
123
  }
124
- };
125
-
126
- // Store original console methods before spying
127
- const originalLog = console.log.bind(console);
128
- const originalError = console.error.bind(console);
129
- const originalWarn = console.warn.bind(console);
130
- const originalInfo = console.info.bind(console);
124
+ }
125
+ }
131
126
 
132
- // Create spies for each console method
133
- const logSpy = vi.spyOn(console, "log").mockImplementation((...args) => {
134
- originalLog(...args); // Call original (Vitest will capture this)
135
- forwardToSandbox(args);
136
- });
127
+ /**
128
+ * Install the singleton console spy (idempotent).
129
+ * Must be called *after* Vitest has set up its own console interception so
130
+ * that the originals we capture are Vitest's wrappers (which feed the test
131
+ * reporter output).
132
+ */
133
+ function installConsoleSpy() {
134
+ if (_consoleSpy.installed) return;
135
+ _consoleSpy.installed = true;
136
+
137
+ // Capture originals once — these are whatever console methods look like
138
+ // right now (possibly already wrapped by Vitest's own reporter).
139
+ _consoleSpy.originals = {
140
+ log: console.log.bind(console),
141
+ error: console.error.bind(console),
142
+ warn: console.warn.bind(console),
143
+ info: console.info.bind(console),
144
+ };
137
145
 
138
- const errorSpy = vi.spyOn(console, "error").mockImplementation((...args) => {
139
- originalError(...args);
140
- forwardToSandbox(args);
141
- });
146
+ const makeHandler = (originalFn) => (...args) => {
147
+ originalFn(...args); // Let Vitest's reporter capture the output
148
+ forwardToAllSandboxes(args); // Forward to all sandbox dashcam streams
149
+ };
142
150
 
143
- const warnSpy = vi.spyOn(console, "warn").mockImplementation((...args) => {
144
- originalWarn(...args);
145
- forwardToSandbox(args);
146
- });
151
+ _consoleSpy.spies = {
152
+ log: vi.spyOn(console, "log").mockImplementation(makeHandler(_consoleSpy.originals.log)),
153
+ error: vi.spyOn(console, "error").mockImplementation(makeHandler(_consoleSpy.originals.error)),
154
+ warn: vi.spyOn(console, "warn").mockImplementation(makeHandler(_consoleSpy.originals.warn)),
155
+ info: vi.spyOn(console, "info").mockImplementation(makeHandler(_consoleSpy.originals.info)),
156
+ };
147
157
 
148
- const infoSpy = vi.spyOn(console, "info").mockImplementation((...args) => {
149
- originalInfo(...args);
150
- forwardToSandbox(args);
151
- });
158
+ if (debugConsoleSpy) {
159
+ process.stdout.write("[DEBUG consoleSpy] Singleton console spy installed\n");
160
+ }
161
+ }
152
162
 
153
- // Store spies on client for cleanup
154
- client._consoleSpies = { logSpy, errorSpy, warnSpy, infoSpy };
163
+ /**
164
+ * Register a TestDriver client so its sandbox receives forwarded logs.
165
+ * @param {import('../../sdk.js').default} client - TestDriver client instance
166
+ * @param {string} taskId - Unique task identifier (for debug logging)
167
+ */
168
+ function setupConsoleSpy(client, taskId) {
169
+ if (debugConsoleSpy) {
170
+ process.stdout.write(`[DEBUG setupConsoleSpy] registering taskId: ${taskId}\n`);
171
+ }
172
+ installConsoleSpy();
173
+ _consoleSpy.activeClients.add(client);
155
174
  }
156
175
 
157
176
  /**
158
- * Clean up console spies and restore original console methods
159
- * @param {TestDriver} client - TestDriver client instance
177
+ * Unregister a client so its sandbox no longer receives forwarded logs.
178
+ * When the last client is removed the spy stays installed (harmless) so we
179
+ * never have to worry about restore-order races with concurrent tests.
180
+ * @param {import('../../sdk.js').default} client - TestDriver client instance
160
181
  */
161
182
  function cleanupConsoleSpy(client) {
162
- if (client._consoleSpies) {
163
- const { logSpy, errorSpy, warnSpy, infoSpy } = client._consoleSpies;
164
- logSpy.mockRestore();
165
- errorSpy.mockRestore();
166
- warnSpy.mockRestore();
167
- infoSpy.mockRestore();
168
- delete client._consoleSpies;
183
+ _consoleSpy.activeClients.delete(client);
184
+ if (debugConsoleSpy) {
185
+ process.stdout.write(
186
+ `[DEBUG cleanupConsoleSpy] clients remaining: ${_consoleSpy.activeClients.size}\n`,
187
+ );
169
188
  }
170
189
  }
171
190
 
@@ -218,7 +237,6 @@ function registerSignalHandlers() {
218
237
  * });
219
238
  */
220
239
  export function TestDriver(context, options = {}) {
221
- console.log("[DEBUG hooks entry] options:", JSON.stringify(options));
222
240
  if (!context || !context.task) {
223
241
  throw new Error(
224
242
  'TestDriver() requires Vitest context. Pass the context parameter from your test function: test("name", async (context) => { ... })',
@@ -274,8 +292,6 @@ export function TestDriver(context, options = {}) {
274
292
  config.apiRoot = process.env.TD_API_ROOT;
275
293
  }
276
294
 
277
- console.log("[DEBUG hooks] options.preview:", options.preview, "config.preview:", config.preview);
278
-
279
295
  const testdriver = new TestDriverSDK(apiKey, config);
280
296
  testdriver.__vitestContext = context.task;
281
297
  testdriver._debugOnFailure = mergedOptions.debugOnFailure || false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testdriverai",
3
- "version": "7.3.28",
3
+ "version": "7.3.29",
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",