From 5672745b76a22f5aea1c2212a99b082ead70e2b5 Mon Sep 17 00:00:00 2001 From: Karan Pradhan <78605930+KaranPradhan266@users.noreply.github.com> Date: Mon, 2 Feb 2026 03:19:19 -0800 Subject: [PATCH 1/2] Mark test script errors as failed in runner (#6261) * Mark test script errors as failed in runner and CLI * Unify handling of post-response and pre-request script errors in both CLI and Electron * feat: Enhance error handling in script execution by preserving partial results for pre-request and post-response scripts across CLI and Electron. This ensures that tests passing before an error are still reported. * Preserving stopExecution in test script error handler --------- Co-authored-by: Pragadesh-45 --- .../src/runner/run-single-request.js | 155 ++++++++++++----- .../bruno-electron/src/ipc/network/index.js | 65 +++++++ .../bruno-js/src/runtime/script-runtime.js | 158 +++++++++++------- 3 files changed, 273 insertions(+), 105 deletions(-) diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 378bd4945..046259b9a 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -206,55 +206,80 @@ const runSingleRequest = async function ( const collectionName = collection?.brunoConfig?.name; if (requestScriptFile?.length) { const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime }); - const result = await scriptRuntime.runRequestScript( - decomment(requestScriptFile), - request, - envVariables, - runtimeVariables, - collectionPath, - onConsoleLog, - processEnvVars, - scriptingConfig, - runSingleRequestByPathname, - collectionName - ); - if (result?.nextRequestName !== undefined) { - nextRequestName = result.nextRequestName; - } + try { + const result = await scriptRuntime.runRequestScript(decomment(requestScriptFile), + request, + envVariables, + runtimeVariables, + collectionPath, + onConsoleLog, + processEnvVars, + scriptingConfig, + runSingleRequestByPathname, + collectionName); + if (result?.nextRequestName !== undefined) { + nextRequestName = result.nextRequestName; + } - if (result?.stopExecution) { - shouldStopRunnerExecution = true; - } + if (result?.stopExecution) { + shouldStopRunnerExecution = true; + } - if (result?.skipRequest) { - return { - test: { - filename: relativeItemPathname - }, - request: { - method: request.method, - url: request.url, - headers: request.headers, - data: request.data - }, - response: { + if (result?.skipRequest) { + return { + test: { + filename: relativeItemPathname + }, + request: { + method: request.method, + url: request.url, + headers: request.headers, + data: request.data + }, + response: { + status: 'skipped', + statusText: 'request skipped via pre-request script', + data: null, + responseTime: 0 + }, + error: null, status: 'skipped', - statusText: 'request skipped via pre-request script', - data: null, - responseTime: 0 - }, - error: null, - status: 'skipped', - skipped: true, - assertionResults: [], - testResults: [], - preRequestTestResults: result?.results || [], - postResponseTestResults: [], - shouldStopRunnerExecution - }; - } + skipped: true, + assertionResults: [], + testResults: [], + preRequestTestResults: result?.results || [], + postResponseTestResults: [], + shouldStopRunnerExecution + }; + } - preRequestTestResults = result?.results || []; + preRequestTestResults = result?.results || []; + } catch (error) { + console.error('Pre-request script execution error:', error); + + // Extract partial results from the error (tests that passed before the error) + const partialResults = error?.partialResults?.results || []; + preRequestTestResults = [ + ...partialResults, + { + status: 'fail', + description: 'Pre-Request Script Error', + error: error.message || 'An error occurred while executing the pre-request script.' + } + ]; + + // Preserve nextRequestName if it was set before the error + if (error?.partialResults?.nextRequestName !== undefined) { + nextRequestName = error.partialResults.nextRequestName; + } + + // Preserve stopExecution if it was set before the error + if (error?.partialResults?.stopExecution) { + shouldStopRunnerExecution = true; + } + + logResults(preRequestTestResults, 'Pre-Request Tests'); + } } // interpolate variables inside request @@ -732,6 +757,26 @@ const runSingleRequest = async function ( logResults(postResponseTestResults, 'Post-Response Tests'); } catch (error) { console.error('Post-response script execution error:', error); + + const partialResults = error?.partialResults?.results || []; + postResponseTestResults = [ + ...partialResults, + { + status: 'fail', + description: 'Post-Response Script Error', + error: error.message || 'An error occurred while executing the post-response script.' + } + ]; + + if (error?.partialResults?.nextRequestName !== undefined) { + nextRequestName = error.partialResults.nextRequestName; + } + + if (error?.partialResults?.stopExecution) { + shouldStopRunnerExecution = true; + } + + logResults(postResponseTestResults, 'Post-Response Tests'); } } @@ -781,6 +826,26 @@ const runSingleRequest = async function ( logResults(testResults, 'Tests'); } catch (error) { console.error('Test script execution error:', error); + + const partialResults = error?.partialResults?.results || []; + testResults = [ + ...partialResults, + { + status: 'fail', + description: 'Test Script Error', + error: error.message || 'An error occurred while executing the test script.' + } + ]; + + if (error?.partialResults?.nextRequestName !== undefined) { + nextRequestName = error.partialResults.nextRequestName; + } + + if (error?.partialResults?.stopExecution) { + shouldStopRunnerExecution = true; + } + + logResults(testResults, 'Tests'); } } diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index ab5d8d3a9..c3c56784d 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -443,6 +443,38 @@ const registerNetworkIpc = (mainWindow) => { }); }; + const appendScriptErrorResult = (scriptType, scriptResult, error) => { + if (!error) { + return scriptResult; + } + + const descriptionMap = { + 'test': 'Test Script Error', + 'post-response': 'Post-Response Script Error', + 'pre-request': 'Pre-Request Script Error' + }; + + const messageMap = { + 'test': 'An error occurred while executing the test script.', + 'post-response': 'An error occurred while executing the post-response script.', + 'pre-request': 'An error occurred while executing the pre-request script.' + }; + + const results = [ + ...(scriptResult?.results || []), + { + status: 'fail', + description: descriptionMap[scriptType] || 'Script Error', + error: error.message || messageMap[scriptType] || 'An error occurred while executing the script.' + } + ]; + + return { + ...(scriptResult || {}), + results + }; + }; + const runPreRequest = async ( request, requestUid, @@ -713,6 +745,12 @@ const registerNetworkIpc = (mainWindow) => { preRequestError = error; } + if (preRequestError?.partialResults) { + preRequestScriptResult = preRequestError.partialResults; + } + + preRequestScriptResult = appendScriptErrorResult('pre-request', preRequestScriptResult, preRequestError); + if (preRequestScriptResult?.results) { mainWindow.webContents.send('main:run-request-event', { type: 'test-results-pre-request', @@ -878,6 +916,15 @@ const registerNetworkIpc = (mainWindow) => { postResponseError = error; } + // Extract partial results from error if available + // This preserves any test() calls that passed before the script errored + // (e.g., if 2 tests pass then script throws, we still want to show those 2 passing tests) + if (postResponseError?.partialResults) { + postResponseScriptResult = postResponseError.partialResults; + } + + postResponseScriptResult = appendScriptErrorResult('post-response', postResponseScriptResult, postResponseError); + if (postResponseScriptResult?.results) { mainWindow.webContents.send('main:run-request-event', { type: 'test-results-post-response', @@ -951,6 +998,8 @@ const registerNetworkIpc = (mainWindow) => { } } + testResults = appendScriptErrorResult('test', testResults, testError); + !runInBackground && mainWindow.webContents.send('main:run-request-event', { type: 'test-results', results: testResults.results, @@ -1336,6 +1385,12 @@ const registerNetworkIpc = (mainWindow) => { preRequestError = error; } + if (preRequestError?.partialResults) { + preRequestScriptResult = preRequestError.partialResults; + } + + preRequestScriptResult = appendScriptErrorResult('pre-request', preRequestScriptResult, preRequestError); + if (preRequestScriptResult?.results) { mainWindow.webContents.send('main:run-folder-event', { type: 'test-results-pre-request', @@ -1558,6 +1613,14 @@ const registerNetworkIpc = (mainWindow) => { postResponseError = error; } + // Extract partial results from error if available + // (e.g., if 2 tests pass then script throws, we still want to show those 2 passing tests) + if (postResponseError?.partialResults) { + postResponseScriptResult = postResponseError.partialResults; + } + + postResponseScriptResult = appendScriptErrorResult('post-response', postResponseScriptResult, postResponseError); + notifyScriptExecution({ channel: 'main:run-folder-event', basePayload: eventData, @@ -1644,6 +1707,8 @@ const registerNetworkIpc = (mainWindow) => { } } + testResults = appendScriptErrorResult('test', testResults, testError); + if (testResults?.nextRequestName !== undefined) { nextRequestName = testResults.nextRequestName; } diff --git a/packages/bruno-js/src/runtime/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js index d41b51284..28cd48e75 100644 --- a/packages/bruno-js/src/runtime/script-runtime.js +++ b/packages/bruno-js/src/runtime/script-runtime.js @@ -68,35 +68,9 @@ class ScriptRuntime { context.bru.runRequest = runRequestByItemPathname; } - if (this.runtime === 'nodevm') { - await runScriptInNodeVm({ - script, - context, - collectionPath, - scriptingConfig - }); - - return { - request, - envVariables: cleanJson(envVariables), - runtimeVariables: cleanJson(runtimeVariables), - persistentEnvVariables: bru.persistentEnvVariables, - globalEnvironmentVariables: cleanJson(globalEnvironmentVariables), - results: cleanJson(__brunoTestResults.getResults()), - nextRequestName: bru.nextRequest, - skipRequest: bru.skipRequest, - stopExecution: bru.stopExecution - }; - } - - // default runtime is `quickjs` - await executeQuickJsVmAsync({ - script: script, - context: context, - collectionPath - }); - - return { + // Helper to build the result object for pre-request scripts + // Extracted to avoid duplication across runtime branches + const buildRequestScriptResult = () => ({ request, envVariables: cleanJson(envVariables), runtimeVariables: cleanJson(runtimeVariables), @@ -106,7 +80,52 @@ class ScriptRuntime { nextRequestName: bru.nextRequest, skipRequest: bru.skipRequest, stopExecution: bru.stopExecution - }; + }); + + // Track script errors to attach partial results before re-throwing + // This ensures that any test() calls that passed before the error are preserved + // Similar pattern to test-runtime.js which already handles this correctly + let scriptError = null; + + if (this.runtime === 'nodevm') { + try { + await runScriptInNodeVm({ + script, + context, + collectionPath, + scriptingConfig + }); + } catch (error) { + scriptError = error; + } + + // If script errored, attach partial results so callers can display passed tests + // before the error occurred (e.g., 2 tests pass, then script throws) + if (scriptError) { + scriptError.partialResults = buildRequestScriptResult(); + throw scriptError; + } + + return buildRequestScriptResult(); + } + + // default runtime is `quickjs` + try { + await executeQuickJsVmAsync({ + script: script, + context: context, + collectionPath + }); + } catch (error) { + scriptError = error; + } + + if (scriptError) { + scriptError.partialResults = buildRequestScriptResult(); + throw scriptError; + } + + return buildRequestScriptResult(); } async runResponseScript( @@ -166,35 +185,9 @@ class ScriptRuntime { context.bru.runRequest = runRequestByItemPathname; } - if (this.runtime === 'nodevm') { - await runScriptInNodeVm({ - script, - context, - collectionPath, - scriptingConfig - }); - - return { - response, - envVariables: cleanJson(envVariables), - persistentEnvVariables: cleanJson(bru.persistentEnvVariables), - runtimeVariables: cleanJson(runtimeVariables), - globalEnvironmentVariables: cleanJson(globalEnvironmentVariables), - results: cleanJson(__brunoTestResults.getResults()), - nextRequestName: bru.nextRequest, - skipRequest: bru.skipRequest, - stopExecution: bru.stopExecution - }; - } - - // default runtime is `quickjs` - await executeQuickJsVmAsync({ - script: script, - context: context, - collectionPath - }); - - return { + // Helper to build the result object for post-response scripts + // Extracted to avoid duplication across runtime branches + const buildResponseScriptResult = () => ({ response, envVariables: cleanJson(envVariables), persistentEnvVariables: cleanJson(bru.persistentEnvVariables), @@ -204,7 +197,52 @@ class ScriptRuntime { nextRequestName: bru.nextRequest, skipRequest: bru.skipRequest, stopExecution: bru.stopExecution - }; + }); + + // Track script errors to attach partial results before re-throwing + // This ensures that any test() calls that passed before the error are preserved + // Similar pattern to test-runtime.js which already handles this correctly + let scriptError = null; + + if (this.runtime === 'nodevm') { + try { + await runScriptInNodeVm({ + script, + context, + collectionPath, + scriptingConfig + }); + } catch (error) { + scriptError = error; + } + + // If script errored, attach partial results so callers can display passed tests + // before the error occurred (e.g., 2 tests pass, then script throws) + if (scriptError) { + scriptError.partialResults = buildResponseScriptResult(); + throw scriptError; + } + + return buildResponseScriptResult(); + } + + // default runtime is `quickjs` + try { + await executeQuickJsVmAsync({ + script: script, + context: context, + collectionPath + }); + } catch (error) { + scriptError = error; + } + + if (scriptError) { + scriptError.partialResults = buildResponseScriptResult(); + throw scriptError; + } + + return buildResponseScriptResult(); } } From 798db041faff7c018ffb27a72d6ae351feb0b877 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <161328623+sanjaikumar-bruno@users.noreply.github.com> Date: Wed, 4 Feb 2026 12:01:42 +0530 Subject: [PATCH 2/2] Enhance error handling for script execution by introducing isScriptError flag in test results (#7029) * fix: Enhance error handling for script execution by introducing isScriptError flag in test results Enhance error reporting in script execution by adding isScriptError flag to error responses fix: Mark pre-request script errors as failures in runner summary --- .../src/runner/run-single-request.js | 47 ++++++++++++++----- .../bruno-common/src/runner/runner-summary.ts | 18 +++++-- .../bruno-common/src/runner/types/index.ts | 2 + .../bruno-electron/src/ipc/network/index.js | 3 +- 4 files changed, 55 insertions(+), 15 deletions(-) diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 046259b9a..29f19155e 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -255,18 +255,12 @@ const runSingleRequest = async function ( preRequestTestResults = result?.results || []; } catch (error) { + // Pre-request errors are treated as request errors (we return early with status: 'error'), not as failures. Unlike post-response and test script errors, we do not add a synthetic fail and continue. console.error('Pre-request script execution error:', error); + console.log(chalk.red(stripExtension(relativeItemPathname)) + chalk.dim(` (${error.message})`)); // Extract partial results from the error (tests that passed before the error) - const partialResults = error?.partialResults?.results || []; - preRequestTestResults = [ - ...partialResults, - { - status: 'fail', - description: 'Pre-Request Script Error', - error: error.message || 'An error occurred while executing the pre-request script.' - } - ]; + preRequestTestResults = error?.partialResults?.results || []; // Preserve nextRequestName if it was set before the error if (error?.partialResults?.nextRequestName !== undefined) { @@ -279,6 +273,35 @@ const runSingleRequest = async function ( } logResults(preRequestTestResults, 'Pre-Request Tests'); + + // Pre-request script error: execution didn't complete (request never sent). Return early so we don't run the HTTP request, post-response script, assertions, or tests. + return { + test: { + filename: relativeItemPathname + }, + request: { + method: request.method, + url: request.url, + headers: request.headers, + data: request.data + }, + response: { + status: 'error', + statusText: null, + headers: null, + data: null, + url: null, + responseTime: 0 + }, + error: error?.message || 'An error occurred while executing the pre-request script.', + status: 'error', + assertionResults: [], + testResults: [], + preRequestTestResults, + postResponseTestResults: [], + nextRequestName: nextRequestName, + shouldStopRunnerExecution + }; } } @@ -764,7 +787,8 @@ const runSingleRequest = async function ( { status: 'fail', description: 'Post-Response Script Error', - error: error.message || 'An error occurred while executing the post-response script.' + error: error.message || 'An error occurred while executing the post-response script.', + isScriptError: true } ]; @@ -833,7 +857,8 @@ const runSingleRequest = async function ( { status: 'fail', description: 'Test Script Error', - error: error.message || 'An error occurred while executing the test script.' + error: error.message || 'An error occurred while executing the test script.', + isScriptError: true } ]; diff --git a/packages/bruno-common/src/runner/runner-summary.ts b/packages/bruno-common/src/runner/runner-summary.ts index 3e99e676f..bde8f8efb 100644 --- a/packages/bruno-common/src/runner/runner-summary.ts +++ b/packages/bruno-common/src/runner/runner-summary.ts @@ -23,10 +23,10 @@ export const getRunnerSummary = (results: T_RunnerRequestExecutionResult[]): T_R for (const result of results || []) { const { status, testResults, assertionResults, preRequestTestResults, postResponseTestResults } = result; totalRequests += 1; - totalTests += Number(testResults?.length) || 0; + totalTests += Number(testResults?.filter((r) => !r.isScriptError).length) || 0; totalAssertions += Number(assertionResults?.length) || 0; - totalPreRequestTests += Number(preRequestTestResults?.length) || 0; - totalPostResponseTests += Number(postResponseTestResults?.length) || 0; + totalPreRequestTests += Number(preRequestTestResults?.filter((r) => !r.isScriptError).length) || 0; + totalPostResponseTests += Number(postResponseTestResults?.filter((r) => !r.isScriptError).length) || 0; if (status === 'skipped') { skippedRequests += 1; @@ -35,6 +35,10 @@ export const getRunnerSummary = (results: T_RunnerRequestExecutionResult[]): T_R let anyFailed = false; for (const testResult of testResults || []) { + if (testResult.isScriptError) { + anyFailed = true; + continue; + } if (testResult.status === 'pass') { passedTests += 1; } else { @@ -51,6 +55,10 @@ export const getRunnerSummary = (results: T_RunnerRequestExecutionResult[]): T_R } } for (const preRequestTestResult of preRequestTestResults || []) { + if (preRequestTestResult.isScriptError) { + anyFailed = true; + continue; + } if (preRequestTestResult.status === 'pass') { passedPreRequestTests += 1; } else { @@ -59,6 +67,10 @@ export const getRunnerSummary = (results: T_RunnerRequestExecutionResult[]): T_R } } for (const postResponseTestResult of postResponseTestResults || []) { + if (postResponseTestResult.isScriptError) { + anyFailed = true; + continue; + } if (postResponseTestResult.status === 'pass') { passedPostResponseTests += 1; } else { diff --git a/packages/bruno-common/src/runner/types/index.ts b/packages/bruno-common/src/runner/types/index.ts index 1b696ba12..0d9b40eb5 100644 --- a/packages/bruno-common/src/runner/types/index.ts +++ b/packages/bruno-common/src/runner/types/index.ts @@ -23,6 +23,7 @@ type T_TestPassResult = { status: string; description: string; uid?: string; + isScriptError?: boolean; }; type T_TestFailResult = { @@ -30,6 +31,7 @@ type T_TestFailResult = { description: string; error: string; uid?: string; + isScriptError?: boolean; }; type T_TestResult = T_TestPassResult | T_TestFailResult; diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index c3c56784d..0fdf02ecc 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -465,7 +465,8 @@ const registerNetworkIpc = (mainWindow) => { { status: 'fail', description: descriptionMap[scriptType] || 'Script Error', - error: error.message || messageMap[scriptType] || 'An error occurred while executing the script.' + error: error.message || messageMap[scriptType] || 'An error occurred while executing the script.', + isScriptError: true } ];