* Execute the test suite and return a structured result. * * @param {object} opts * @param {string} [opts.test_filter] - Optional name/pattern filter * @param {string} [opts.workdir] - Working directory (default: process.cwd()) * @param {number} [opts.timeout] - Timeout in ms (def
(opts = {})
| 323 | * @returns {object} Structured test result |
| 324 | */ |
| 325 | function runTests(opts = {}) { |
| 326 | const cwd = opts.workdir || process.cwd(); |
| 327 | const timeout = opts.timeout || 120000; |
| 328 | const test_filter = opts.test_filter || null; |
| 329 | |
| 330 | const detector = getTestRunnerDetector({ workdir: cwd }); |
| 331 | const runner = detector.detect(); |
| 332 | |
| 333 | if (!runner) { |
| 334 | return { |
| 335 | passed: 0, failed: 0, errors: 0, skipped: 0, failures: [], |
| 336 | raw: 'No test runner detected. Create a pytest.ini, package.json with a test script, Cargo.toml, go.mod, etc.', |
| 337 | exitCode: -1, durationMs: 0, command: null, framework: null, |
| 338 | summary: 'No test runner detected.', |
| 339 | }; |
| 340 | } |
| 341 | |
| 342 | const filterArg = buildFilterArg(runner.framework, test_filter); |
| 343 | const command = runner.command + filterArg; |
| 344 | |
| 345 | const start = Date.now(); |
| 346 | let rawOutput = ''; |
| 347 | let exitCode = 0; |
| 348 | |
| 349 | try { |
| 350 | rawOutput = execSync(command, { |
| 351 | encoding: 'utf-8', |
| 352 | timeout, |
| 353 | cwd, |
| 354 | maxBuffer: 2 * 1024 * 1024, |
| 355 | // Merge stderr into stdout so we capture everything |
| 356 | stdio: ['ignore', 'pipe', 'pipe'], |
| 357 | }); |
| 358 | } catch (e) { |
| 359 | rawOutput = (e.stdout || '') + (e.stderr || ''); |
| 360 | exitCode = typeof e.status === 'number' ? e.status : 1; |
| 361 | } |
| 362 | |
| 363 | const durationMs = Date.now() - start; |
| 364 | const raw = sanitizeToolOutput(rawOutput).slice(0, 3000); |
| 365 | const parsed = parseOutput(runner.framework, rawOutput, exitCode); |
| 366 | |
| 367 | const { passed, failed, errors, skipped, failures } = parsed; |
| 368 | |
| 369 | // Build a one-line summary the model can parse at a glance |
| 370 | const parts = []; |
| 371 | if (passed > 0) parts.push(`${passed} passed`); |
| 372 | if (failed > 0) parts.push(`${failed} failed`); |
| 373 | if (errors > 0) parts.push(`${errors} errors`); |
| 374 | if (skipped > 0) parts.push(`${skipped} skipped`); |
| 375 | const summary = parts.length > 0 ? parts.join(', ') : (exitCode === 0 ? 'all passed' : 'failed (no tests found or parse error)'); |
| 376 | |
| 377 | return { |
| 378 | passed, |
| 379 | failed, |
| 380 | errors, |
| 381 | skipped, |
| 382 | failures: failures.slice(0, 20), // cap to avoid overwhelming context |
no test coverage detected