From 304f9db93a0f372af36e811d8dfd20a21069695f Mon Sep 17 00:00:00 2001 From: Anish Sinha Date: Tue, 11 Aug 2026 10:20:31 +0530 Subject: [PATCH] fix(rerun): send project-relative spec paths to remote on re-run [SDK-7124] BROWSERSTACK_RERUN_TESTS arrives as bare filenames ("a.ts,b.ts"). run_settings is forwarded to the BrowserStack machines verbatim, where a bare filename does not resolve against the project root, so Cypress exits having run no spec at all ("Cypress could not run any of the specs"). getNumberOfSpecFiles already resolves those entries against cypressProjectDir via matchBase globbing, but only persisted the resolved list back to run_settings.specs under turboScaleSession. Persist it for re-run sessions too, so the remote receives paths it can resolve. Scoped to re-runs via a new shared predicate isReRunSpecsSession(), which setUserSpecs now uses as well so the two call sites cannot drift. A plain --spec glob is deliberately left untouched. Co-Authored-By: Claude Opus 5 --- bin/helpers/utils.js | 22 ++++++++++- test/unit/bin/helpers/utils.js | 70 ++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/bin/helpers/utils.js b/bin/helpers/utils.js index a356a5b8..2cc7f133 100644 --- a/bin/helpers/utils.js +++ b/bin/helpers/utils.js @@ -491,10 +491,20 @@ exports.setNodeVersion = (bsConfig, args) => { } // specs can be passed from bstack configuration file +// True when the spec list for this run comes from BROWSERSTACK_RERUN_TESTS rather than +// the user's config/CLI args. Shared by setUserSpecs and getNumberOfSpecFiles so the two +// cannot drift apart — getNumberOfSpecFiles rewrites specs only for runs setUserSpecs +// actually sourced from the re-run env. +exports.isReRunSpecsSession = () => { + return o11yHelpers.isBrowserstackInfra() + && o11yHelpers.isTestObservabilitySession() + && o11yHelpers.shouldReRunObservabilityTests(); +} + // specs can be passed via command line args as a string // command line args takes precedence over config exports.setUserSpecs = (bsConfig, args) => { - if(o11yHelpers.isBrowserstackInfra() && o11yHelpers.isTestObservabilitySession() && o11yHelpers.shouldReRunObservabilityTests()) { + if(this.isReRunSpecsSession()) { // BROWSERSTACK_RERUN_TESTS arrives comma+space separated (e.g. "a.ts, b.ts"); normalise // like the other spec sources below, else sanitizeSpecsPattern builds "{a.ts, b.ts}" whose // space-prefixed brace alternatives never match and the failed-spec filter collapses. @@ -1232,6 +1242,16 @@ exports.getNumberOfSpecFiles = (bsConfig, args, cypressConfig, turboScaleSession files = files.map((x) => { return x.replaceAll("\\", "/") }) // setting specs for turboScale as we don't have patched API for turboscale so we will rely on info from CLI bsConfig.run_settings.specs = files; + } else if (this.isReRunSpecsSession() && files.length) { + // BROWSERSTACK_RERUN_TESTS arrives as bare filenames ("a.ts,b.ts"). run_settings is + // forwarded to the remote machines verbatim, and a bare filename does not resolve + // against the project root there, so Cypress exits having run no spec at all. The + // glob above already resolved them against cypressProjectDir — persist those + // project-relative paths so the remote receives something it can resolve. + bsConfig.run_settings.specs = files + .map((file) => path.relative(bsConfig.run_settings.cypressProjectDir, file).replaceAll("\\", "/")) + .join(","); + logger.debug(`Re-run specs resolved to project-relative paths: ${bsConfig.run_settings.specs}`); } return files; } catch (err) { diff --git a/test/unit/bin/helpers/utils.js b/test/unit/bin/helpers/utils.js index 2705d770..480cb5f4 100644 --- a/test/unit/bin/helpers/utils.js +++ b/test/unit/bin/helpers/utils.js @@ -2327,6 +2327,76 @@ describe('utils', () => { }); describe('getNumberOfSpecFiles', () => { + context('when re-running observability failed tests (SDK-7124)', () => { + let rerunStubs = []; + const specPattern = `cypress/e2e/**/*.+(${constant.specFileTypes.join('|')})`; + + const stubGlob = () => { + const globStub = sinon.stub(glob, 'sync'); + globStub.withArgs(specPattern).returns([ + 'cypress/e2e/FlightOffer/FO-E2E-02.ts', + 'cypress/e2e/FlightOffer/FO-E2E-05.ts', + 'cypress/e2e/FlightOffer/FO-E2E-09.ts', + ]); + globStub.withArgs('{FO-E2E-02.ts,FO-E2E-05.ts}').returns([ + 'cypress/e2e/FlightOffer/FO-E2E-02.ts', + 'cypress/e2e/FlightOffer/FO-E2E-05.ts', + ]); + return globStub; + }; + + const buildConfig = (specs) => ({ + run_settings: { + cypressTestSuiteType: CYPRESS_V10_AND_ABOVE_TYPE, + specs: specs, + cypressProjectDir: '/repo', + }, + }); + + beforeEach(() => { + rerunStubs.push(sinon.stub(o11yHelpers, 'isBrowserstackInfra').returns(true)); + rerunStubs.push(sinon.stub(o11yHelpers, 'isTestObservabilitySession').returns(true)); + rerunStubs.push(sinon.stub(o11yHelpers, 'shouldReRunObservabilityTests').returns(true)); + }); + + afterEach(() => { + rerunStubs.forEach((s) => s.restore()); + rerunStubs = []; + if (glob.sync.restore) glob.sync.restore(); + }); + + it('rewrites the bare rerun filenames to project-relative paths for the remote machines', () => { + stubGlob(); + // shape setUserSpecs leaves behind for a rerun: bare filenames, comma separated + let bsConfig = buildConfig('FO-E2E-02.ts,FO-E2E-05.ts'); + + const result = utils.getNumberOfSpecFiles(bsConfig, {}, {}); + + expect(result.length).to.eql(2); + // run_settings is forwarded to the remote verbatim, so it must carry resolvable paths + expect(bsConfig.run_settings.specs).to.be.eq( + 'cypress/e2e/FlightOffer/FO-E2E-02.ts,cypress/e2e/FlightOffer/FO-E2E-05.ts' + ); + }); + + it('leaves specs untouched when this is not a rerun session', () => { + rerunStubs.forEach((s) => s.restore()); + rerunStubs = []; + sinon.stub(o11yHelpers, 'isBrowserstackInfra').returns(false); + sinon.stub(o11yHelpers, 'isTestObservabilitySession').returns(false); + sinon.stub(o11yHelpers, 'shouldReRunObservabilityTests').returns(false); + rerunStubs.push(o11yHelpers.isBrowserstackInfra); + rerunStubs.push(o11yHelpers.isTestObservabilitySession); + rerunStubs.push(o11yHelpers.shouldReRunObservabilityTests); + stubGlob(); + let bsConfig = buildConfig('FO-E2E-02.ts,FO-E2E-05.ts'); + + utils.getNumberOfSpecFiles(bsConfig, {}, {}); + + expect(bsConfig.run_settings.specs).to.be.eq('FO-E2E-02.ts,FO-E2E-05.ts'); + }); + }); + it('should return files matching with run_settings.specs and under default folder if cypress v <= 9 and no integration/testFiles patterm provided', () => { let globStub = sinon.stub(glob, 'sync') globStub.withArgs('cypress/integration/foo*.js')