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 +4 -0
- package/interfaces/vitest-plugin.mjs +22 -10
- package/lib/vitest/hooks.mjs +108 -92
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -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
|
-
|
|
1304
|
-
const
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
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
|
|
package/lib/vitest/hooks.mjs
CHANGED
|
@@ -59,113 +59,132 @@ function checkVitestVersion() {
|
|
|
59
59
|
checkVitestVersion();
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
85
|
-
let forwardedCount = 0;
|
|
86
|
-
let skippedCount = 0;
|
|
83
|
+
const debugConsoleSpy = process.env.TD_DEBUG_CONSOLE_SPY === "true";
|
|
87
84
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
158
|
+
if (debugConsoleSpy) {
|
|
159
|
+
process.stdout.write("[DEBUG consoleSpy] Singleton console spy installed\n");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
152
162
|
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
*
|
|
159
|
-
*
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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;
|