From 4a962b7c5272873c5aa93113be7796657d1eab4a Mon Sep 17 00:00:00 2001 From: DudaGod Date: Fri, 25 Sep 2026 00:15:09 +0300 Subject: [PATCH 1/2] fix(runner): fail tests on background session errors --- src/worker/runner/test-runner/index.js | 19 ++++++++++++++ test/src/worker/runner/test-runner/index.js | 28 +++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/worker/runner/test-runner/index.js b/src/worker/runner/test-runner/index.js index 7a022dadf..78a0385bd 100644 --- a/src/worker/runner/test-runner/index.js +++ b/src/worker/runner/test-runner/index.js @@ -14,6 +14,7 @@ const { filterExtraStackFrames } = require("../../../browser/stacktrace/utils"); const { extendWithCodeSnippet } = require("../../../error-snippets"); const { startSelectivity } = require("../../../browser/cdp/selectivity"); const { noopProfilerRuntime } = require("../../../profiler/runtime/noop"); +const { flushSessionManagerErrors } = require("@testplane/webdriverio"); const SNAPSHOTS_TIMEOUT_MS = 10000; const SNAPSHOTS_WARNING_TIMEOUT_MS = 2000; @@ -108,6 +109,20 @@ module.exports = class TestRunner { async finishRun(error) { const testplaneCtx = this._test.testplaneCtx; const { callstackHistory } = this._browser; + let additionalContextError; + + // Older WebdriverIO versions do not expose the test-boundary check. + if (typeof flushSessionManagerErrors === "function") { + try { + await flushSessionManagerErrors(this._browser.publicAPI); + } catch (contextError) { + if (error && error !== contextError) { + additionalContextError = contextError; + } else { + error = contextError; + } + } + } const assertViewResults = testplaneCtx.assertViewResults; if (!error && assertViewResults && assertViewResults.hasFails()) { @@ -172,6 +187,10 @@ module.exports = class TestRunner { // The original cause must be removed to avoid possible duplicates later. delete error.cause; + if (additionalContextError) { + error.stack += `\n\nAdditional context manager error: ${additionalContextError.stack}`; + } + await extendWithCodeSnippet(error); throw Object.assign(error, results); diff --git a/test/src/worker/runner/test-runner/index.js b/test/src/worker/runner/test-runner/index.js index 42b517a8f..0d9ef7344 100644 --- a/test/src/worker/runner/test-runner/index.js +++ b/test/src/worker/runner/test-runner/index.js @@ -19,6 +19,7 @@ describe("worker/runner/test-runner", () => { const sandbox = sinon.createSandbox(); let historyRunGroupStub; let captureFailScreenshotStub; + let flushSessionManagerErrorsStub; let TestRunner; const mkTest_ = (opts = {}) => { @@ -91,6 +92,7 @@ describe("worker/runner/test-runner", () => { beforeEach(() => { historyRunGroupStub = sandbox.stub().callsFake(history.runGroup); captureFailScreenshotStub = sandbox.stub().resolves(null); + flushSessionManagerErrorsStub = sandbox.stub().resolves(); TestRunner = proxyquire("src/worker/runner/test-runner", { "../../../browser/history": { @@ -102,6 +104,9 @@ describe("worker/runner/test-runner", () => { "./capture-fail-screenshot": { captureFailScreenshot: captureFailScreenshotStub, }, + "@testplane/webdriverio": { + flushSessionManagerErrors: flushSessionManagerErrorsStub, + }, }); sandbox.stub(BrowserAgent.prototype, "getBrowser").resolves(mkBrowser_()); @@ -838,6 +843,19 @@ describe("worker/runner/test-runner", () => { assert.calledOnceWith(BrowserAgent.prototype.freeBrowser, browser); }); + it("should fail on a context manager error after the last command", async () => { + const browser = mkBrowser_(); + BrowserAgent.prototype.getBrowser.resolves(browser); + flushSessionManagerErrorsStub.rejects(new Error("late context failure")); + + await assert.isRejected(run_(), "late context failure"); + + assert.calledOnceWithExactly(flushSessionManagerErrorsStub, browser.publicAPI); + assert.calledOnceWithExactly(BrowserAgent.prototype.freeBrowser, browser); + assert.callOrder(flushSessionManagerErrorsStub, BrowserAgent.prototype.freeBrowser); + assert.isTrue(browser.state.isLastTestFailed); + }); + it("should release browser after all browser data has been used", async () => { ExecutionThread.create.callsFake(({ browser }) => { ExecutionThread.prototype.run.callsFake(() => { @@ -976,6 +994,16 @@ describe("worker/runner/test-runner", () => { assert.calledOnceWith(BrowserAgent.prototype.freeBrowser, browser); }); + it("should retain the original test error when context manager also fails", async () => { + ExecutionThread.prototype.run.rejects(new Error("test failure")); + flushSessionManagerErrorsStub.rejects(new Error("context failure")); + + const error = await run_().catch(err => err); + assert.include(error.message, "test failure"); + assert.include(error.stack, "context failure"); + assert.calledOnce(flushSessionManagerErrorsStub); + }); + [SAVE_HISTORY_MODE.ONLY_FAILED, SAVE_HISTORY_MODE.ALL].forEach(saveHistoryMode => { it(`should pass history if "saveHistory" is ${saveHistoryMode}`, async () => { const history = Symbol(); From 3b777d85a4743144533721c7ae6572a5e9fd40a6 Mon Sep 17 00:00:00 2001 From: DudaGod Date: Fri, 25 Sep 2026 00:39:20 +0300 Subject: [PATCH 2/2] fix(runner): discard sessions after context errors --- src/worker/runner/test-runner/index.js | 5 +++++ test/src/worker/runner/test-runner/index.js | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/worker/runner/test-runner/index.js b/src/worker/runner/test-runner/index.js index 78a0385bd..df9577c29 100644 --- a/src/worker/runner/test-runner/index.js +++ b/src/worker/runner/test-runner/index.js @@ -116,6 +116,11 @@ module.exports = class TestRunner { try { await flushSessionManagerErrors(this._browser.publicAPI); } catch (contextError) { + // A failed context update may leave the session pointing at a stale frame. + // The master pool discards browsers marked as broken instead of reusing them. + if (!this._browser.state.isBroken) { + this._browser.markAsBroken({ stubBrowserCommands: true }); + } if (error && error !== contextError) { additionalContextError = contextError; } else { diff --git a/test/src/worker/runner/test-runner/index.js b/test/src/worker/runner/test-runner/index.js index 0d9ef7344..bbfdc938d 100644 --- a/test/src/worker/runner/test-runner/index.js +++ b/test/src/worker/runner/test-runner/index.js @@ -854,6 +854,8 @@ describe("worker/runner/test-runner", () => { assert.calledOnceWithExactly(BrowserAgent.prototype.freeBrowser, browser); assert.callOrder(flushSessionManagerErrorsStub, BrowserAgent.prototype.freeBrowser); assert.isTrue(browser.state.isLastTestFailed); + assert.calledOnceWithExactly(browser.markAsBroken, { stubBrowserCommands: true }); + assert.isTrue(browser.state.isBroken); }); it("should release browser after all browser data has been used", async () => { @@ -995,6 +997,8 @@ describe("worker/runner/test-runner", () => { }); it("should retain the original test error when context manager also fails", async () => { + const browser = mkBrowser_(); + BrowserAgent.prototype.getBrowser.resolves(browser); ExecutionThread.prototype.run.rejects(new Error("test failure")); flushSessionManagerErrorsStub.rejects(new Error("context failure")); @@ -1002,6 +1006,7 @@ describe("worker/runner/test-runner", () => { assert.include(error.message, "test failure"); assert.include(error.stack, "context failure"); assert.calledOnce(flushSessionManagerErrorsStub); + assert.calledOnceWithExactly(browser.markAsBroken, { stubBrowserCommands: true }); }); [SAVE_HISTORY_MODE.ONLY_FAILED, SAVE_HISTORY_MODE.ALL].forEach(saveHistoryMode => {