From 48700f4bdc06df2037e0a5c32998bd703216adf5 Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Wed, 15 Feb 2023 20:32:15 -0500 Subject: [PATCH 01/52] cycle through preflight requests --- packages/server/lib/cloud/api.ts | 43 +++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts index 179ae5cbdfa2..834733f79f78 100644 --- a/packages/server/lib/cloud/api.ts +++ b/packages/server/lib/cloud/api.ts @@ -454,21 +454,34 @@ module.exports = { preflight (preflightInfo) { return retryWithBackoff(async (attemptIndex) => { - const preflightBase = process.env.CYPRESS_API_URL ? apiUrl.replace('api', 'api-proxy') : apiUrl - const result = await rp.post({ - url: `${preflightBase}preflight`, - body: { - apiUrl, - envUrl: process.env.CYPRESS_API_URL, - ...preflightInfo, - }, - headers: { - 'x-route-version': '1', - 'x-cypress-request-attempt': attemptIndex, - }, - json: true, - encrypt: 'always', - }) + const preflightBaseProxy = process.env.CYPRESS_API_URL ? apiUrl.replace('api', 'api-proxy') : apiUrl + + const makeReq = (baseUrl) => { + return rp.post({ + url: `${baseUrl}preflight`, + body: { + apiUrl, + envUrl: process.env.CYPRESS_API_URL, + ...preflightInfo, + }, + headers: { + 'x-route-version': '1', + 'x-cypress-request-attempt': attemptIndex, + }, + json: true, + encrypt: 'always', + }) + } + + const postReqs = async () => { + try { + return makeReq(preflightBaseProxy) + } catch (e) { + return makeReq(apiUrl) + } + } + + const result = await postReqs() preflightResult = result // { encrypt: boolean, apiUrl: string } recordRoutes = makeRoutes(result.apiUrl) From c2b647a71c5e9c299aec47df86cda1beb927cf79 Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Wed, 15 Feb 2023 20:35:19 -0500 Subject: [PATCH 02/52] always replace apiUrl --- packages/server/lib/cloud/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts index 834733f79f78..3cfe3b1d7b29 100644 --- a/packages/server/lib/cloud/api.ts +++ b/packages/server/lib/cloud/api.ts @@ -454,7 +454,7 @@ module.exports = { preflight (preflightInfo) { return retryWithBackoff(async (attemptIndex) => { - const preflightBaseProxy = process.env.CYPRESS_API_URL ? apiUrl.replace('api', 'api-proxy') : apiUrl + const preflightBaseProxy = apiUrl.replace('api', 'api-proxy') const makeReq = (baseUrl) => { return rp.post({ From c080a1171fa2c5dd48de2602266d9e5a62a6c4f2 Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Wed, 15 Feb 2023 23:59:51 -0500 Subject: [PATCH 03/52] fix logic, add additional test cases --- packages/server/lib/cloud/api.ts | 23 ++-- packages/server/test/unit/cloud/api_spec.js | 116 ++++++++++++++++---- 2 files changed, 111 insertions(+), 28 deletions(-) diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts index 3cfe3b1d7b29..a230b972fdd1 100644 --- a/packages/server/lib/cloud/api.ts +++ b/packages/server/lib/cloud/api.ts @@ -88,8 +88,13 @@ const rp = request.defaults((params: CypressRequestOptions, callback) => { if (params.encrypt === true || params.encrypt === 'always') { const { secretKey, jwe } = await enc.encryptRequest(params) + // TODO: double check the logic below here with @tgriesser params.transform = async function (body, response) { - if (response.headers['x-cypress-encrypted'] || params.encrypt === 'always' && response.statusCode < 500) { + // if response is valid + if (response.statusCode < 500 && + // ...and we are encrypting + (response.headers['x-cypress-encrypted'] || params.encrypt === 'always') + ) { let decryptedBody try { @@ -100,6 +105,7 @@ const rp = request.defaults((params: CypressRequestOptions, callback) => { // If we've hit an encrypted payload error case, we need to re-constitute the error // as it would happen normally, with the body as an error property + // TODO: need to look harder at this to better understand why its necessary if (response.statusCode > 400) { throw new RequestErrors.StatusCodeError(response.statusCode, decryptedBody, {}, decryptedBody) } @@ -228,7 +234,6 @@ export type CreateRunOptions = { let preflightResult = { encrypt: true, - apiUrl, } let recordRoutes = apiRoutes @@ -244,11 +249,11 @@ module.exports = { } }, + // TODO: i think we can remove this function resetPreflightResult () { recordRoutes = apiRoutes preflightResult = { encrypt: true, - apiUrl, } }, @@ -272,7 +277,8 @@ module.exports = { createRun (options: CreateRunOptions) { const preflightOptions = _.pick(options, ['projectId', 'ciBuildId', 'browser', 'testingType', 'parallel']) - return this.preflight(preflightOptions).then((result) => { + return this.postPreflight(preflightOptions) + .then((result) => { const { warnings } = result return retryWithBackoff((attemptIndex) => { @@ -452,7 +458,7 @@ module.exports = { responseCache = {} }, - preflight (preflightInfo) { + postPreflight (preflightInfo) { return retryWithBackoff(async (attemptIndex) => { const preflightBaseProxy = apiUrl.replace('api', 'api-proxy') @@ -474,11 +480,10 @@ module.exports = { } const postReqs = async () => { - try { - return makeReq(preflightBaseProxy) - } catch (e) { + return makeReq(preflightBaseProxy) + .catch((err) => { return makeReq(apiUrl) - } + }) } const result = await postReqs() diff --git a/packages/server/test/unit/cloud/api_spec.js b/packages/server/test/unit/cloud/api_spec.js index 8bb4347075dd..1a0f567d57a9 100644 --- a/packages/server/test/unit/cloud/api_spec.js +++ b/packages/server/test/unit/cloud/api_spec.js @@ -1,6 +1,7 @@ const crypto = require('crypto') const jose = require('jose') const base64Url = require('base64url') +const stealthyRequire = require('stealthy-require') require('../../spec_helper') @@ -19,6 +20,8 @@ const machineId = require('../../../lib/cloud/machine_id') const Promise = require('bluebird') const API_BASEURL = 'http://localhost:1234' +const API_PROD_BASEURL = 'https://api.cypress.io' +const API_PROD_PROXY_BASEURL = 'https://api-proxy.cypress.io' const CLOUD_BASEURL = 'http://localhost:3000' const AUTH_URLS = { 'dashboardAuthUrl': 'http://localhost:3000/test-runner.html', @@ -29,18 +32,23 @@ const makeError = (details = {}) => { return _.extend(new Error(details.message || 'Some error'), details) } -const preflightNock = (encrypted = false) => { +const decryptResponse = ({ body, encrypted }) => { const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048, }) - const encryptRequest = encryption.encryptRequest /** - * @type {crypto.KeyObject} - */ + * @type {crypto.KeyObject} + */ let _secretKey + const encryptRequest = encryption.encryptRequest + sinon.stub(encryption, 'encryptRequest').callsFake(async (params) => { + if (body) { + expect(params.body).to.deep.eq(body) + } + const { secretKey, jwe } = await encryptRequest(params, publicKey) _secretKey = secretKey @@ -48,13 +56,7 @@ const preflightNock = (encrypted = false) => { return { secretKey, jwe } }) - nock(API_BASEURL) - .defaultReplyHeaders({ 'x-cypress-encrypted': 'true' }) - .matchHeader('x-route-version', '1') - .matchHeader('x-os-name', 'linux') - .matchHeader('x-cypress-version', pkg.version) - .post('/preflight', () => true) - .reply(200, async (uri, requestBody) => { + return async (uri, requestBody) => { const decryptedSecretKey = crypto.createSecretKey( crypto.privateDecrypt( privateKey, @@ -74,13 +76,24 @@ const preflightNock = (encrypted = false) => { const jweResponse = await enc.encrypt() return jweResponse - }) + } +} + +const preflightNock = (baseUrl) => { + return nock(baseUrl) + .defaultReplyHeaders({ 'x-cypress-encrypted': 'true' }) + .matchHeader('x-route-version', '1') + .matchHeader('x-os-name', 'linux') + .matchHeader('x-cypress-version', pkg.version) + .post('/preflight') } describe('lib/cloud/api', () => { beforeEach(() => { api.setPreflightResult({ encrypt: false }) - preflightNock(false) + + preflightNock(API_BASEURL) + .reply(200, decryptResponse({ encrypted: false })) nock(API_BASEURL) .matchHeader('x-route-version', '2') @@ -196,17 +209,82 @@ describe('lib/cloud/api', () => { }) }) - context('.preflight', () => { - it('POST /preflight + returns encryption', function () { + context('.postPreflight', () => { + let prodApi + + beforeEach(() => { nock.cleanAll() sinon.restore() - sinon.stub(os, 'platform').returns('linux') - preflightNock(true) - return api.preflight({ projectId: 'abc123' }) + process.env.CYPRESS_CONFIG_ENV = 'production' + process.env.CYPRESS_API_URL = 'https://some.server.com' + + prodApi = stealthyRequire(require.cache, () => { + return require('../../../lib/cloud/api') + }, () => { + require('../../../lib/cloud/encryption') + }, module) + }) + + it('POST /preflight to proxy. returns encryption', function () { + preflightNock(API_PROD_PROXY_BASEURL) + .reply(200, decryptResponse({ + encrypted: true, + body: { + envUrl: 'https://some.server.com', // TODO: fix this + apiUrl: 'https://api.cypress.io/', + projectId: 'abc123', + }, + })) + + return prodApi.postPreflight({ projectId: 'abc123' }) + .then((ret) => { + expect(ret).to.deep.eq({ encrypted: true, apiUrl: `${API_PROD_BASEURL}/` }) + }) + }) + + it('POST /preflight to proxy, and then api on response status code failure. returns encryption', function () { + const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL) + .reply(500) + + const scopeApi = preflightNock(API_PROD_BASEURL) + .reply(200, decryptResponse({ + encrypted: true, + body: { + envUrl: 'https://some.server.com', // TODO: fix this + apiUrl: 'https://api.cypress.io/', + projectId: 'abc123', + }, + })) + + return prodApi.postPreflight({ projectId: 'abc123' }) + .then((ret) => { + scopeProxy.done() + scopeApi.done() + expect(ret).to.deep.eq({ encrypted: true, apiUrl: `${API_PROD_BASEURL}/` }) + }) + }) + + it('POST /preflight to proxy, and then api on network failure. returns encryption', function () { + const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL) + .replyWithError('some request error') + + const scopeApi = preflightNock(API_PROD_BASEURL) + .reply(200, decryptResponse({ + encrypted: true, + body: { + envUrl: 'https://some.server.com', // TODO: fix this + apiUrl: 'https://api.cypress.io/', + projectId: 'abc123', + }, + })) + + return prodApi.postPreflight({ projectId: 'abc123' }) .then((ret) => { - expect(ret).to.deep.eq({ encrypted: true, apiUrl: `${API_BASEURL}/` }) + scopeProxy.done() + scopeApi.done() + expect(ret).to.deep.eq({ encrypted: true, apiUrl: `${API_PROD_BASEURL}/` }) }) }) }) From ecbde1e460bdd3b61b2002eef136a91139a2a811 Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Thu, 16 Feb 2023 00:00:20 -0500 Subject: [PATCH 04/52] add tests for default timeout on /preflight --- packages/server/lib/cloud/api.ts | 17 +++++++++----- packages/server/test/unit/cloud/api_spec.js | 26 +++++++++++++++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts index a230b972fdd1..6a6e2b971e03 100644 --- a/packages/server/lib/cloud/api.ts +++ b/packages/server/lib/cloud/api.ts @@ -275,7 +275,7 @@ module.exports = { }, createRun (options: CreateRunOptions) { - const preflightOptions = _.pick(options, ['projectId', 'ciBuildId', 'browser', 'testingType', 'parallel']) + const preflightOptions = _.pick(options, ['projectId', 'ciBuildId', 'browser', 'testingType', 'parallel', 'timeout']) return this.postPreflight(preflightOptions) .then((result) => { @@ -306,7 +306,7 @@ module.exports = { url: recordRoutes.runs(), json: true, encrypt: preflightResult.encrypt, - timeout: options.timeout != null ? options.timeout : SIXTY_SECONDS, + timeout: options.timeout ?? SIXTY_SECONDS, headers: { 'x-route-version': '4', 'x-cypress-request-attempt': attemptIndex, @@ -340,7 +340,7 @@ module.exports = { url: recordRoutes.instances(runId), json: true, encrypt: preflightResult.encrypt, - timeout: timeout != null ? timeout : SIXTY_SECONDS, + timeout: timeout ?? SIXTY_SECONDS, headers: { 'x-route-version': '5', 'x-cypress-run-id': runId, @@ -360,7 +360,7 @@ module.exports = { url: recordRoutes.instanceTests(instanceId), json: true, encrypt: preflightResult.encrypt, - timeout: timeout || SIXTY_SECONDS, + timeout: timeout ?? SIXTY_SECONDS, headers: { 'x-route-version': '1', 'x-cypress-run-id': runId, @@ -378,7 +378,7 @@ module.exports = { return rp.put({ url: recordRoutes.instanceStdout(options.instanceId), json: true, - timeout: options.timeout != null ? options.timeout : SIXTY_SECONDS, + timeout: options.timeout ?? SIXTY_SECONDS, body: { stdout: options.stdout, }, @@ -399,7 +399,7 @@ module.exports = { url: recordRoutes.instanceResults(options.instanceId), json: true, encrypt: preflightResult.encrypt, - timeout: options.timeout != null ? options.timeout : SIXTY_SECONDS, + timeout: options.timeout ?? SIXTY_SECONDS, headers: { 'x-route-version': '1', 'x-cypress-run-id': options.runId, @@ -460,6 +460,10 @@ module.exports = { postPreflight (preflightInfo) { return retryWithBackoff(async (attemptIndex) => { + const { timeout } = preflightInfo + + preflightInfo = _.omit(preflightInfo, 'timeout') + const preflightBaseProxy = apiUrl.replace('api', 'api-proxy') const makeReq = (baseUrl) => { @@ -474,6 +478,7 @@ module.exports = { 'x-route-version': '1', 'x-cypress-request-attempt': attemptIndex, }, + timeout: timeout ?? SIXTY_SECONDS, json: true, encrypt: 'always', }) diff --git a/packages/server/test/unit/cloud/api_spec.js b/packages/server/test/unit/cloud/api_spec.js index 1a0f567d57a9..0f1e48a88a48 100644 --- a/packages/server/test/unit/cloud/api_spec.js +++ b/packages/server/test/unit/cloud/api_spec.js @@ -287,6 +287,32 @@ describe('lib/cloud/api', () => { expect(ret).to.deep.eq({ encrypted: true, apiUrl: `${API_PROD_BASEURL}/` }) }) }) + + it('handles timeout', () => { + preflightNock(API_BASEURL) + .times(2) + .delayConnection(5000) + .reply(200, {}) + + return api.postPreflight({ + timeout: 100, + }) + .then(() => { + throw new Error('should have thrown here') + }) + .catch((err) => { + expect(err.message).to.eq('Error: ESOCKETTIMEDOUT') + }) + }) + + it('sets timeout to 60 seconds', () => { + sinon.stub(api.rp, 'post').resolves({}) + + return api.postPreflight({}) + .then(() => { + expect(api.rp.post).to.be.calledWithMatch({ timeout: 60000 }) + }) + }) }) context('.createRun', () => { From 29e6951333e7df5cb4c157908ea3cd7cc80223d0 Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Thu, 16 Feb 2023 00:55:49 -0500 Subject: [PATCH 05/52] fix tests, rename function stub --- packages/server/test/integration/cypress_spec.js | 2 +- packages/server/test/unit/modes/record_spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/test/integration/cypress_spec.js b/packages/server/test/integration/cypress_spec.js index ff060586b67f..1ee92f33e05a 100644 --- a/packages/server/test/integration/cypress_spec.js +++ b/packages/server/test/integration/cypress_spec.js @@ -1196,7 +1196,7 @@ describe('lib/cypress', () => { beforeEach(async function () { await clearCtx() - sinon.stub(api, 'preflight').resolves() + sinon.stub(api, 'postPreflight').resolves() sinon.stub(api, 'createRun').resolves() const createInstanceStub = sinon.stub(api, 'createInstance') diff --git a/packages/server/test/unit/modes/record_spec.js b/packages/server/test/unit/modes/record_spec.js index 9f8b040bfb4e..55bb06115ec8 100644 --- a/packages/server/test/unit/modes/record_spec.js +++ b/packages/server/test/unit/modes/record_spec.js @@ -17,7 +17,7 @@ const initialEnv = _.clone(process.env) // tested as an e2e/record_spec describe('lib/modes/record', () => { beforeEach(() => { - sinon.stub(api, 'preflight').callsFake(async () => { + sinon.stub(api, 'postPreflight').callsFake(async () => { api.setPreflightResult({ encrypt: false }) }) }) From b2a047c923a41fd61302e37cebc3fb6c9fbe3895 Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Thu, 16 Feb 2023 00:56:14 -0500 Subject: [PATCH 06/52] optimize only instantiating apiProd once --- packages/server/test/unit/cloud/api_spec.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/server/test/unit/cloud/api_spec.js b/packages/server/test/unit/cloud/api_spec.js index 0f1e48a88a48..60473ad459d0 100644 --- a/packages/server/test/unit/cloud/api_spec.js +++ b/packages/server/test/unit/cloud/api_spec.js @@ -212,7 +212,9 @@ describe('lib/cloud/api', () => { context('.postPreflight', () => { let prodApi - beforeEach(() => { + beforeEach(function () { + this.timeout(30000) + nock.cleanAll() sinon.restore() sinon.stub(os, 'platform').returns('linux') @@ -220,11 +222,13 @@ describe('lib/cloud/api', () => { process.env.CYPRESS_CONFIG_ENV = 'production' process.env.CYPRESS_API_URL = 'https://some.server.com' - prodApi = stealthyRequire(require.cache, () => { - return require('../../../lib/cloud/api') - }, () => { - require('../../../lib/cloud/encryption') - }, module) + if (!prodApi) { + prodApi = stealthyRequire(require.cache, () => { + return require('../../../lib/cloud/api') + }, () => { + require('../../../lib/cloud/encryption') + }, module) + } }) it('POST /preflight to proxy. returns encryption', function () { From d01987b1427c03a8c7a10e7412bcc0f768938034 Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Thu, 16 Feb 2023 02:44:18 -0500 Subject: [PATCH 07/52] general cleanup --- packages/server/lib/cloud/api.ts | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts index 6a6e2b971e03..939f30b40cd3 100644 --- a/packages/server/lib/cloud/api.ts +++ b/packages/server/lib/cloud/api.ts @@ -20,11 +20,9 @@ const THIRTY_SECONDS = humanInterval('30 seconds') const SIXTY_SECONDS = humanInterval('60 seconds') const TWO_MINUTES = humanInterval('2 minutes') -const DELAYS: number[] = process.env.API_RETRY_INTERVALS ? process.env.API_RETRY_INTERVALS.split(',').map(_.toNumber) : [ - THIRTY_SECONDS, - SIXTY_SECONDS, - TWO_MINUTES, -] +const DELAYS: number[] = process.env.API_RETRY_INTERVALS + ? process.env.API_RETRY_INTERVALS.split(',').map(_.toNumber) + : [THIRTY_SECONDS, SIXTY_SECONDS, TWO_MINUTES] const runnerCapabilities = { 'dynamicSpecsInSerialMode': true, @@ -35,6 +33,7 @@ let responseCache = {} class DecryptionError extends Error { isDecryptionError = true + constructor (message: string) { super(message) this.name = 'DecryptionError' @@ -51,7 +50,7 @@ const rp = request.defaults((params: CypressRequestOptions, callback) => { let resp if (params.cacheable && (resp = getCachedResponse(params))) { - debug('resolving with cached response for ', params.url) + debug('resolving with cached response for %o', { url: params.url }) return Bluebird.resolve(resp) } @@ -65,7 +64,7 @@ const rp = request.defaults((params: CypressRequestOptions, callback) => { rejectUnauthorized: true, }) - const headers = params.headers != null ? params.headers : (params.headers = {}) + const headers = params.headers ??= {} _.defaults(headers, { 'x-os-name': os.platform(), @@ -142,16 +141,13 @@ const getCachedResponse = (params) => { } const retryWithBackoff = (fn) => { - // for e2e testing purposes - let attempt - if (process.env.DISABLE_API_RETRIES) { debug('api retries disabled') return Bluebird.try(() => fn(0)) } - return (attempt = (retryIndex) => { + const attempt = (retryIndex) => { return Bluebird .try(() => fn(retryIndex)) .catch(isRetriableError, (err) => { @@ -182,7 +178,9 @@ const retryWithBackoff = (fn) => { .catch(RequestErrors.TransformError, (err) => { throw err.cause }) - })(0) + } + + return attempt(0) } const formatResponseBody = function (err) { From f5bcc6eaca882da5d4fccb214ce84d8e9c818564 Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Thu, 16 Feb 2023 02:47:46 -0500 Subject: [PATCH 08/52] general cleanup and refactoring of test helpers --- packages/server/test/unit/cloud/api_spec.js | 62 +++++++++++++-------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/packages/server/test/unit/cloud/api_spec.js b/packages/server/test/unit/cloud/api_spec.js index 60473ad459d0..31ac15522406 100644 --- a/packages/server/test/unit/cloud/api_spec.js +++ b/packages/server/test/unit/cloud/api_spec.js @@ -32,7 +32,7 @@ const makeError = (details = {}) => { return _.extend(new Error(details.message || 'Some error'), details) } -const decryptResponse = ({ body, encrypted }) => { +const decryptReqBodyAndRespond = ({ reqBody, resBody }) => { const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048, }) @@ -45,8 +45,8 @@ const decryptResponse = ({ body, encrypted }) => { const encryptRequest = encryption.encryptRequest sinon.stub(encryption, 'encryptRequest').callsFake(async (params) => { - if (body) { - expect(params.body).to.deep.eq(body) + if (reqBody) { + expect(params.body).to.deep.eq(reqBody) } const { secretKey, jwe } = await encryptRequest(params, publicKey) @@ -56,19 +56,18 @@ const decryptResponse = ({ body, encrypted }) => { return { secretKey, jwe } }) - return async (uri, requestBody) => { + return async (uri, encReqBody) => { const decryptedSecretKey = crypto.createSecretKey( crypto.privateDecrypt( privateKey, - Buffer.from(base64Url.toBase64(requestBody.recipients[0].encrypted_key), 'base64'), + Buffer.from(base64Url.toBase64(encReqBody.recipients[0].encrypted_key), 'base64'), ), ) - const decrypted = await encryption.decryptResponse(requestBody, privateKey) expect(_secretKey.export().toString('utf8')).to.eq(decryptedSecretKey.export().toString('utf8')) const enc = new jose.GeneralEncrypt( - Buffer.from(JSON.stringify({ encrypted, apiUrl: decrypted.apiUrl })), + Buffer.from(JSON.stringify(resBody)), ) enc.setProtectedHeader({ alg: 'A256GCMKW', enc: 'A256GCM', zip: 'DEF' }).addRecipient(decryptedSecretKey) @@ -93,7 +92,12 @@ describe('lib/cloud/api', () => { api.setPreflightResult({ encrypt: false }) preflightNock(API_BASEURL) - .reply(200, decryptResponse({ encrypted: false })) + .reply(200, decryptReqBodyAndRespond({ + resBody: { + encrypted: false, + apiUrl: `${API_BASEURL}/`, + }, + })) nock(API_BASEURL) .matchHeader('x-route-version', '2') @@ -231,15 +235,18 @@ describe('lib/cloud/api', () => { } }) - it('POST /preflight to proxy. returns encryption', function () { + it('POST /preflight to proxy. returns encryption', () => { preflightNock(API_PROD_PROXY_BASEURL) - .reply(200, decryptResponse({ - encrypted: true, - body: { + .reply(200, decryptReqBodyAndRespond({ + reqBody: { envUrl: 'https://some.server.com', // TODO: fix this apiUrl: 'https://api.cypress.io/', projectId: 'abc123', }, + resBody: { + encrypted: true, + apiUrl: `${API_PROD_BASEURL}/`, + }, })) return prodApi.postPreflight({ projectId: 'abc123' }) @@ -248,18 +255,21 @@ describe('lib/cloud/api', () => { }) }) - it('POST /preflight to proxy, and then api on response status code failure. returns encryption', function () { + it('POST /preflight to proxy, and then api on response status code failure. returns encryption', () => { const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL) .reply(500) const scopeApi = preflightNock(API_PROD_BASEURL) - .reply(200, decryptResponse({ - encrypted: true, - body: { + .reply(200, decryptReqBodyAndRespond({ + reqBody: { envUrl: 'https://some.server.com', // TODO: fix this apiUrl: 'https://api.cypress.io/', projectId: 'abc123', }, + resBody: { + encrypted: true, + apiUrl: `${API_PROD_BASEURL}/`, + }, })) return prodApi.postPreflight({ projectId: 'abc123' }) @@ -270,18 +280,21 @@ describe('lib/cloud/api', () => { }) }) - it('POST /preflight to proxy, and then api on network failure. returns encryption', function () { + it('POST /preflight to proxy, and then api on network failure. returns encryption', () => { const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL) .replyWithError('some request error') const scopeApi = preflightNock(API_PROD_BASEURL) - .reply(200, decryptResponse({ - encrypted: true, - body: { + .reply(200, decryptReqBodyAndRespond({ + reqBody: { envUrl: 'https://some.server.com', // TODO: fix this apiUrl: 'https://api.cypress.io/', projectId: 'abc123', }, + resBody: { + encrypted: true, + apiUrl: `${API_PROD_BASEURL}/`, + }, })) return prodApi.postPreflight({ projectId: 'abc123' }) @@ -1060,13 +1073,16 @@ describe('lib/cloud/api', () => { return api.retryWithBackoff(fn1) .then(() => { throw new Error('Should not resolve 499 error') - }).catch((err) => { + }) + .catch((err) => { expect(err.message).to.equal('499 error') return api.retryWithBackoff(fn2) - }).then(() => { + }) + .then(() => { throw new Error('Should not resolve 600 error') - }).catch((err) => { + }) + .catch((err) => { expect(err.message).to.equal('600 error') }) }) From 1462c86185613f6c07daf07912472f16db36d8db Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Thu, 16 Feb 2023 02:48:17 -0500 Subject: [PATCH 09/52] add tests and logic for additional error scenarios --- packages/server/lib/cloud/api.ts | 21 ++- packages/server/test/unit/cloud/api_spec.js | 168 ++++++++++++++++++-- 2 files changed, 165 insertions(+), 24 deletions(-) diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts index 939f30b40cd3..f20aebde3993 100644 --- a/packages/server/lib/cloud/api.ts +++ b/packages/server/lib/cloud/api.ts @@ -89,13 +89,21 @@ const rp = request.defaults((params: CypressRequestOptions, callback) => { // TODO: double check the logic below here with @tgriesser params.transform = async function (body, response) { - // if response is valid - if (response.statusCode < 500 && - // ...and we are encrypting - (response.headers['x-cypress-encrypted'] || params.encrypt === 'always') - ) { + const { statusCode } = response + + // if response is invalid or not found + // then bail and do nothing + // and let this error bubble up + if (statusCode >= 500 || statusCode === 404) { + return body + } + + // response is valid and we are encrypting + if (response.headers['x-cypress-encrypted'] || params.encrypt === 'always') { let decryptedBody + // TODO: if body is null/undefined throw a custom error + try { decryptedBody = await enc.decryptResponse(body, secretKey) } catch (e) { @@ -111,8 +119,6 @@ const rp = request.defaults((params: CypressRequestOptions, callback) => { return decryptedBody } - - return body } params.body = jwe @@ -175,6 +181,7 @@ const retryWithBackoff = (fn) => { return attempt(retryIndex) }) }) + // TODO: look at this too .catch(RequestErrors.TransformError, (err) => { throw err.cause }) diff --git a/packages/server/test/unit/cloud/api_spec.js b/packages/server/test/unit/cloud/api_spec.js index 31ac15522406..d873f6806507 100644 --- a/packages/server/test/unit/cloud/api_spec.js +++ b/packages/server/test/unit/cloud/api_spec.js @@ -305,23 +305,6 @@ describe('lib/cloud/api', () => { }) }) - it('handles timeout', () => { - preflightNock(API_BASEURL) - .times(2) - .delayConnection(5000) - .reply(200, {}) - - return api.postPreflight({ - timeout: 100, - }) - .then(() => { - throw new Error('should have thrown here') - }) - .catch((err) => { - expect(err.message).to.eq('Error: ESOCKETTIMEDOUT') - }) - }) - it('sets timeout to 60 seconds', () => { sinon.stub(api.rp, 'post').resolves({}) @@ -330,6 +313,143 @@ describe('lib/cloud/api', () => { expect(api.rp.post).to.be.calledWithMatch({ timeout: 60000 }) }) }) + + describe('errors', () => { + it('handles timeout', () => { + preflightNock(API_BASEURL) + .times(2) + .delayConnection(5000) + .reply(200, {}) + + return api.postPreflight({ + timeout: 100, + }) + .then(() => { + throw new Error('should have thrown here') + }) + .catch((err) => { + expect(err.message).to.eq('Error: ESOCKETTIMEDOUT') + }) + }) + + it('[F1] POST /preflight RequestError', () => { + const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL) + .replyWithError('first request error') + + const scopeApi = preflightNock(API_PROD_BASEURL) + .replyWithError('2nd request error') + + return prodApi.postPreflight({ projectId: 'abc123' }) + .then(() => { + throw new Error('should have thrown here') + }) + .catch((err) => { + scopeProxy.done() + scopeApi.done() + + expect(err).not.to.have.property('statusCode') + expect(err).to.contain({ + name: 'RequestError', + message: 'Error: 2nd request error', + }) + }) + }) + + it('[F1] POST /preflight statusCode >= 500', () => { + const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL) + .reply(500) + + const scopeApi = preflightNock(API_PROD_BASEURL) + .reply(500) + + return prodApi.postPreflight({ projectId: 'abc123' }) + .then(() => { + throw new Error('should have thrown here') + }) + .catch((err) => { + scopeProxy.done() + scopeApi.done() + + expect(err).to.contain({ + name: 'StatusCodeError', + statusCode: 500, + }) + }) + }) + + it('[F2] POST /preflight statusCode = 404', () => { + const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL) + .reply(404) + + const scopeApi = preflightNock(API_PROD_BASEURL) + .reply(404) + + return prodApi.postPreflight({ projectId: 'abc123' }) + .then(() => { + throw new Error('should have thrown here') + }) + .catch((err) => { + scopeProxy.done() + scopeApi.done() + + expect(err).to.contain({ + name: 'StatusCodeError', + statusCode: 404, + }) + }) + }) + + // TODO: finish implementing this test + it.skip('[F3] POST /preflight statusCode = 422/412', () => { + + }) + + it('[F4] POST /preflight statusCode OK but decrypt error', () => { + const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL) + .reply(200, { data: 'very encrypted and secure string' }) + + const scopeApi = preflightNock(API_PROD_BASEURL) + .reply(201, 'very encrypted and secure string') + + return prodApi.postPreflight({ projectId: 'abc123' }) + .then(() => { + throw new Error('should have thrown here') + }) + .catch((err) => { + scopeProxy.done() + scopeApi.done() + + expect(err).not.to.have.property('statusCode') + expect(err).to.contain({ + name: 'TransformError', + message: 'DecryptionError: General JWE must be an object', + }) + }) + }) + + it('[F5] POST /preflight statusCode OK but no body', () => { + const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL) + .reply(200) + + const scopeApi = preflightNock(API_PROD_BASEURL) + .reply(201) + + return prodApi.postPreflight({ projectId: 'abc123' }) + .then(() => { + throw new Error('should have thrown here') + }) + .catch((err) => { + scopeProxy.done() + scopeApi.done() + + expect(err).not.to.have.property('statusCode') + expect(err).to.contain({ + name: 'TransformError', + message: 'DecryptionError: General JWE must be an object', + }) + }) + }) + }) }) context('.createRun', () => { @@ -451,6 +571,20 @@ describe('lib/cloud/api', () => { expect(err.isApiError).to.be.true }) }) + + it('tags errors on /preflight', function () { + preflightNock(API_BASEURL) + .times(2) + .reply(500, {}) + + return api.createRun({}) + .then(() => { + throw new Error('should have thrown here') + }) + .catch((err) => { + expect(err.isApiError).to.be.true + }) + }) }) context('.createInstance', () => { From 72d4b166c92cc192e886a2f283f32d17f568edad Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Thu, 16 Feb 2023 03:43:49 -0500 Subject: [PATCH 10/52] cleanup tests, add tests for ensuring encrypt is respected on subsequent requests --- packages/server/test/unit/cloud/api_spec.js | 57 +++++++++++++++++---- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/packages/server/test/unit/cloud/api_spec.js b/packages/server/test/unit/cloud/api_spec.js index d873f6806507..bd27df68c6e5 100644 --- a/packages/server/test/unit/cloud/api_spec.js +++ b/packages/server/test/unit/cloud/api_spec.js @@ -32,7 +32,7 @@ const makeError = (details = {}) => { return _.extend(new Error(details.message || 'Some error'), details) } -const decryptReqBodyAndRespond = ({ reqBody, resBody }) => { +const decryptReqBodyAndRespond = ({ reqBody, resBody }, fn) => { const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048, }) @@ -51,6 +51,8 @@ const decryptReqBodyAndRespond = ({ reqBody, resBody }) => { const { secretKey, jwe } = await encryptRequest(params, publicKey) + encryption.encryptRequest.restore() + _secretKey = secretKey return { secretKey, jwe } @@ -74,13 +76,14 @@ const decryptReqBodyAndRespond = ({ reqBody, resBody }) => { const jweResponse = await enc.encrypt() + fn && fn() + return jweResponse } } const preflightNock = (baseUrl) => { return nock(baseUrl) - .defaultReplyHeaders({ 'x-cypress-encrypted': 'true' }) .matchHeader('x-route-version', '1') .matchHeader('x-os-name', 'linux') .matchHeader('x-cypress-version', pkg.version) @@ -94,7 +97,7 @@ describe('lib/cloud/api', () => { preflightNock(API_BASEURL) .reply(200, decryptReqBodyAndRespond({ resBody: { - encrypted: false, + encrypt: false, apiUrl: `${API_BASEURL}/`, }, })) @@ -244,14 +247,14 @@ describe('lib/cloud/api', () => { projectId: 'abc123', }, resBody: { - encrypted: true, + encrypt: true, apiUrl: `${API_PROD_BASEURL}/`, }, })) return prodApi.postPreflight({ projectId: 'abc123' }) .then((ret) => { - expect(ret).to.deep.eq({ encrypted: true, apiUrl: `${API_PROD_BASEURL}/` }) + expect(ret).to.deep.eq({ encrypt: true, apiUrl: `${API_PROD_BASEURL}/` }) }) }) @@ -267,7 +270,7 @@ describe('lib/cloud/api', () => { projectId: 'abc123', }, resBody: { - encrypted: true, + encrypt: true, apiUrl: `${API_PROD_BASEURL}/`, }, })) @@ -276,7 +279,7 @@ describe('lib/cloud/api', () => { .then((ret) => { scopeProxy.done() scopeApi.done() - expect(ret).to.deep.eq({ encrypted: true, apiUrl: `${API_PROD_BASEURL}/` }) + expect(ret).to.deep.eq({ encrypt: true, apiUrl: `${API_PROD_BASEURL}/` }) }) }) @@ -292,7 +295,7 @@ describe('lib/cloud/api', () => { projectId: 'abc123', }, resBody: { - encrypted: true, + encrypt: true, apiUrl: `${API_PROD_BASEURL}/`, }, })) @@ -301,7 +304,7 @@ describe('lib/cloud/api', () => { .then((ret) => { scopeProxy.done() scopeApi.done() - expect(ret).to.deep.eq({ encrypted: true, apiUrl: `${API_PROD_BASEURL}/` }) + expect(ret).to.deep.eq({ encrypt: true, apiUrl: `${API_PROD_BASEURL}/` }) }) }) @@ -382,7 +385,9 @@ describe('lib/cloud/api', () => { .reply(404) const scopeApi = preflightNock(API_PROD_BASEURL) - .reply(404) + .reply(404, '404 not found', { + 'Content-Type': 'text/html', + }) return prodApi.postPreflight({ projectId: 'abc123' }) .then(() => { @@ -498,6 +503,38 @@ describe('lib/cloud/api', () => { }) }) + it('POST /runs + returns runId with encryption', function () { + nock.cleanAll() + sinon.restore() + sinon.stub(os, 'platform').returns('linux') + + preflightNock(API_BASEURL) + .reply(200, decryptReqBodyAndRespond({ + resBody: { + encrypt: true, + apiUrl: `${API_BASEURL}/`, + }, + }, () => { + nock(API_BASEURL) + .defaultReplyHeaders({ 'x-cypress-encrypted': 'true' }) + .matchHeader('x-route-version', '4') + .matchHeader('x-os-name', 'linux') + .matchHeader('x-cypress-version', pkg.version) + .post('/runs') + .reply(200, decryptReqBodyAndRespond({ + reqBody: this.buildProps, + resBody: { + runId: 'new-run-id-123', + }, + })) + })) + + return api.createRun(this.buildProps) + .then((ret) => { + expect(ret).to.deep.eq({ runId: 'new-run-id-123' }) + }) + }) + it('POST /runs failure formatting', function () { nock(API_BASEURL) .matchHeader('x-route-version', '4') From 8c5817f9ab65684fe5aee36c3f8a2b5f07088ff8 Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Thu, 16 Feb 2023 05:18:51 -0500 Subject: [PATCH 11/52] formatting --- packages/server/lib/modes/record.js | 5 ++--- packages/server/test/unit/cloud/api_spec.js | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/server/lib/modes/record.js b/packages/server/lib/modes/record.js index 0097820aa678..c08206113650 100644 --- a/packages/server/lib/modes/record.js +++ b/packages/server/lib/modes/record.js @@ -384,9 +384,8 @@ const createRun = Promise.method((options = {}) => { } }) }).catch((err) => { - debug('failed creating run with status %d %o', err.statusCode, { - stack: err.stack, - }) + debug('failed creating run with status %o', + _.pick(err, ['name', 'message', 'statusCode', 'stack'])) switch (err.statusCode) { case 401: diff --git a/packages/server/test/unit/cloud/api_spec.js b/packages/server/test/unit/cloud/api_spec.js index bd27df68c6e5..2b15e267edaa 100644 --- a/packages/server/test/unit/cloud/api_spec.js +++ b/packages/server/test/unit/cloud/api_spec.js @@ -38,8 +38,8 @@ const decryptReqBodyAndRespond = ({ reqBody, resBody }, fn) => { }) /** - * @type {crypto.KeyObject} - */ + * @type {crypto.KeyObject} + */ let _secretKey const encryptRequest = encryption.encryptRequest From 60984ebaa814783ecdd4c5b34fa70617962b3b4d Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Thu, 16 Feb 2023 08:09:01 -0500 Subject: [PATCH 12/52] bugfix: prevent retryable API errors from retrying one too many times --- packages/server/lib/cloud/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts index f20aebde3993..dc869e0b552c 100644 --- a/packages/server/lib/cloud/api.ts +++ b/packages/server/lib/cloud/api.ts @@ -157,7 +157,7 @@ const retryWithBackoff = (fn) => { return Bluebird .try(() => fn(retryIndex)) .catch(isRetriableError, (err) => { - if (retryIndex > DELAYS.length) { + if (retryIndex >= DELAYS.length) { throw err } From 139eda5142f8b097553ba2520097ab37fe2a5a89 Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Thu, 16 Feb 2023 08:14:35 -0500 Subject: [PATCH 13/52] cleaned up various error messages - made some errors clearer - moved up server response errors to be closer to the error, instead of pushed down by list items - renamed old usage of "Desktop App" to "Cypress app" - updated on links referring to the dashboard -> cloud --- packages/errors/src/errors.ts | 108 +++++++++--------- packages/server/lib/cloud/api.ts | 8 +- system-tests/__snapshots__/record_spec.js | 131 +++++++++------------- 3 files changed, 105 insertions(+), 142 deletions(-) diff --git a/packages/errors/src/errors.ts b/packages/errors/src/errors.ts index 42f46ac6b9ec..e67874d0e83f 100644 --- a/packages/errors/src/errors.ts +++ b/packages/errors/src/errors.ts @@ -56,7 +56,7 @@ export const AllCypressErrors = { return errTemplate`\ Warning: We failed to trash the existing run results. - This error will not alter the exit code. + This error will not affect or change the exit code. ${fmt.stackTrace(arg1)}` }, @@ -64,7 +64,7 @@ export const AllCypressErrors = { return errTemplate`\ Warning: We failed to remove old browser profiles from previous runs. - This error will not alter the exit code. + This error will not affect or change the exit code. ${fmt.stackTrace(arg1)}` }, @@ -72,7 +72,7 @@ export const AllCypressErrors = { return errTemplate`\ Warning: We failed to record the video. - This error will not alter the exit code. + This error will not affect or change the exit code. ${fmt.stackTrace(arg1)}` }, @@ -80,7 +80,7 @@ export const AllCypressErrors = { return errTemplate`\ Warning: We failed processing this video. - This error will not alter the exit code. + This error will not affect or change the exit code. ${fmt.stackTrace(arg1)}` }, @@ -134,7 +134,7 @@ export const AllCypressErrors = { return errTemplate`\ You're not logged in. - Run ${fmt.highlight(`cypress open`)} to open the Desktop App and log in.` + Run ${fmt.highlight(`cypress open`)} to open Cypress and log in.` }, TESTS_DID_NOT_START_RETRYING: (arg1: string) => { return errTemplate`Timed out waiting for the browser to connect. ${fmt.off(arg1)}` @@ -145,52 +145,48 @@ export const AllCypressErrors = { CLOUD_CANCEL_SKIPPED_SPEC: () => { return errTemplate`${fmt.off(`\n `)}This spec and its tests were skipped because the run has been canceled.` }, - CLOUD_API_RESPONSE_FAILED_RETRYING: (arg1: {tries: number, delay: number, response: Error}) => { + CLOUD_API_RESPONSE_FAILED_RETRYING: (arg1: {tries: number, delayMs: number, response: Error}) => { const time = pluralize('time', arg1.tries) - const delay = humanTime.long(arg1.delay, false) + const delay = humanTime.long(arg1.delayMs, false) return errTemplate`\ - We encountered an unexpected error talking to our servers. + We encountered an unexpected error communicating with our servers. - We will retry ${fmt.off(arg1.tries)} more ${fmt.off(time)} in ${fmt.off(delay)}... + ${fmt.highlightSecondary(arg1.response)} - The server's response was: - - ${fmt.highlightSecondary(arg1.response)}` + We will retry ${fmt.off(arg1.tries)} more ${fmt.off(time)} in ${fmt.off(delay)}...` /* Because of fmt.listFlags() and fmt.listItems() */ /* eslint-disable indent */ }, CLOUD_CANNOT_PROCEED_IN_PARALLEL: (arg1: {flags: any, response: Error}) => { return errTemplate`\ - We encountered an unexpected error talking to our servers. + We encountered an unexpected error communicating with our servers. + + ${fmt.highlightSecondary(arg1.response)} Because you passed the ${fmt.flag(`--parallel`)} flag, this run cannot proceed because it requires a valid response from our servers. ${fmt.listFlags(arg1.flags, { group: '--group', ciBuildId: '--ciBuildId', - })} - - The server's response was: - - ${fmt.highlightSecondary(arg1.response)}` + })}` }, CLOUD_CANNOT_PROCEED_IN_SERIAL: (arg1: {flags: any, response: Error}) => { return errTemplate`\ - We encountered an unexpected error talking to our servers. + We encountered an unexpected error communicating with our servers. + + ${fmt.highlightSecondary(arg1.response)} ${fmt.listFlags(arg1.flags, { group: '--group', ciBuildId: '--ciBuildId', - })} - - The server's response was: - - ${fmt.highlightSecondary(arg1.response)}` + })}` }, CLOUD_UNKNOWN_INVALID_REQUEST: (arg1: {flags: any, response: Error}) => { return errTemplate`\ - We encountered an unexpected error talking to our servers. + We encountered an unexpected error communicating with our servers. + + ${fmt.highlightSecondary(arg1.response)} There is likely something wrong with the request. @@ -199,11 +195,7 @@ export const AllCypressErrors = { group: '--group', parallel: '--parallel', ciBuildId: '--ciBuildId', - })} - - The server's response was: - - ${fmt.highlightSecondary(arg1.response)}` + })}` }, CLOUD_UNKNOWN_CREATE_RUN_WARNING: (arg1: {props?: any, message: string}) => { if (!Object.keys(arg1.props).length) { @@ -362,15 +354,15 @@ export const AllCypressErrors = { ${fmt.highlightSecondary(`Auto Cancellation`)} is not included under your current billing plan. To enable this service, please visit your billing and upgrade to another plan with Auto Cancellation. - + ${fmt.off(arg1.link)}` }, CLOUD_AUTO_CANCEL_MISMATCH: (arg1: {runUrl: string}) => { return errTemplate`\ You passed the ${fmt.flag(`--auto-cancel-after-failures`)} flag, but this run originally started with a different value for the ${fmt.flag(`--auto-cancel-after-failures`)} flag. - + The existing run is: ${fmt.url(arg1.runUrl)} - + ${fmt.listFlags(arg1, { tags: '--tag', group: '--group', @@ -380,7 +372,7 @@ export const AllCypressErrors = { })} The first setting of --auto-cancel-after-failures for any given run takes precedent. - + https://on.cypress.io/auto-cancellation-mismatch` }, DEPRECATED_BEFORE_BROWSER_LAUNCH_ARGS: () => { @@ -498,7 +490,7 @@ export const AllCypressErrors = { }, CLOUD_INVALID_RUN_REQUEST: (arg1: {message: string, errors: string[], object: object}) => { return errTemplate`\ - Recording this run failed because the request was invalid. + Recording this run failed. The request was invalid. ${fmt.highlight(arg1.message)} @@ -518,7 +510,7 @@ export const AllCypressErrors = { These results will not be recorded. - This error will not alter the exit code.` + This error will not affect or change the exit code.` }, CLOUD_CANNOT_UPLOAD_RESULTS: (apiErr: Error) => { return errTemplate`\ @@ -526,17 +518,17 @@ export const AllCypressErrors = { These results will not be recorded. - This error will not alter the exit code. + This error will not affect or change the exit code. ${fmt.highlightSecondary(apiErr)}` }, CLOUD_CANNOT_CREATE_RUN_OR_INSTANCE: (apiErr: Error) => { return errTemplate`\ - Warning: We encountered an error talking to our servers. + Warning: We encountered an error communicating with our servers. - This run will not be recorded. + This run will proceed, but will not be recorded. - This error will not alter the exit code. + This error will not affect or change the exit code. ${fmt.highlightSecondary(apiErr)}` }, @@ -560,9 +552,9 @@ export const AllCypressErrors = { We will list the correct projectId in the 'Settings' tab. - Alternatively, you can create a new project using the Desktop Application. + Alternatively, you can create a new project directly from within the Cypress app. - https://on.cypress.io/dashboard` + https://on.cypress.io/cloud` }, // TODO: make this relative path, not absolute NO_PROJECT_ID: (configFilePath: string) => { @@ -880,7 +872,7 @@ export const AllCypressErrors = { CONFIG_FILES_LANGUAGE_CONFLICT: (projectRoot: string, filesFound: string[]) => { return errTemplate` Could not load a Cypress configuration file because there are multiple matches. - + We've found ${fmt.highlight(filesFound.length)} Cypress configuration files named ${fmt.highlight(filesFound.join(', '))} at the location below: @@ -1142,7 +1134,7 @@ export const AllCypressErrors = { The ${fmt.highlight(`experimentalSessionSupport`)} configuration option was removed in ${fmt.cypressVersion(`9.6.0`)}. You can safely remove this option from your config. - + https://on.cypress.io/session` }, EXPERIMENTAL_SESSION_AND_ORIGIN_REMOVED: () => { @@ -1150,7 +1142,7 @@ export const AllCypressErrors = { The ${fmt.highlight(`experimentalSessionAndOrigin`)} configuration option was removed in ${fmt.cypressVersion(`12.0.0`)}. You can safely remove this option from your config. - + https://on.cypress.io/session https://on.cypress.io/origin` }, @@ -1174,10 +1166,10 @@ export const AllCypressErrors = { }, EXPERIMENTAL_STUDIO_REMOVED: () => { return errTemplate`\ - We're ending the experimental phase of Cypress Studio in ${fmt.cypressVersion(`10.0.0`)}. - + We're ending the experimental phase of Cypress Studio in ${fmt.cypressVersion(`10.0.0`)}. + If you don't think you can live without Studio or you'd like to learn about how to work around its removal, please join the discussion here: http://on.cypress.io/studio-removal - + Your feedback will help us factor in product decisions that may see Studio return in a future release. You can safely remove the ${fmt.highlight(`experimentalStudio`)} configuration option from your config.` @@ -1401,11 +1393,11 @@ export const AllCypressErrors = { return errTemplate`\ The ${fmt.highlight('pluginsFile')} configuration option you have supplied has been replaced with ${fmt.highlightSecondary('setupNodeEvents')}. - + This new option is not a one-to-one correlation and it must be configured separately as a testing type property: ${fmt.highlightSecondary('e2e.setupNodeEvents')} and ${fmt.highlightSecondary('component.setupNodeEvents')} - + ${fmt.code(code)} - + https://on.cypress.io/migration-guide` }, @@ -1566,7 +1558,7 @@ export const AllCypressErrors = { UNEXPECTED_INTERNAL_ERROR: (err: Error) => { return errTemplate` - We encountered an unexpected internal error. Please check GitHub or open a new issue + We encountered an unexpected internal error. Please check GitHub or open a new issue if you don't see one already with the details below: ${fmt.stackTrace(err)} @@ -1618,7 +1610,7 @@ export const AllCypressErrors = { ${fmt.code(code)} https://on.cypress.io/migration-guide - + ${stackTrace} ` }, @@ -1662,14 +1654,14 @@ export const AllCypressErrors = { ${fmt.code(code)} https://on.cypress.io/migration-guide - + ${stackTrace} ` }, MIGRATION_MISMATCHED_CYPRESS_VERSIONS: (version: string, currentVersion: string) => { return errTemplate` - You are running ${fmt.cypressVersion(currentVersion)} in global mode, but you are attempting to migrate a project where ${fmt.cypressVersion(version)} is installed. + You are running ${fmt.cypressVersion(currentVersion)} in global mode, but you are attempting to migrate a project where ${fmt.cypressVersion(version)} is installed. Ensure the project you are migrating has Cypress version ${fmt.cypressVersion(currentVersion)} installed. @@ -1692,14 +1684,14 @@ export const AllCypressErrors = { return errTemplate`\ You are using ${fmt.highlight(devServer)} for your dev server, but a configuration file was not found. We traversed upwards from: - + ${fmt.highlightSecondary(root)} - + looking for a file named: ${fmt.listItems(searchedFor, { prefix: ' - ' })} - Add your ${fmt.highlight(devServer)} config at one of the above paths, or import your configuration file and provide it to + Add your ${fmt.highlight(devServer)} config at one of the above paths, or import your configuration file and provide it to the devServer config as a ${fmt.highlight(devServerConfigFile)} option. ` }, diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts index dc869e0b552c..891de1db98a1 100644 --- a/packages/server/lib/cloud/api.ts +++ b/packages/server/lib/cloud/api.ts @@ -161,11 +161,11 @@ const retryWithBackoff = (fn) => { throw err } - const delay = DELAYS[retryIndex] + const delayMs = DELAYS[retryIndex] errors.warning( 'CLOUD_API_RESPONSE_FAILED_RETRYING', { - delay, + delayMs, tries: DELAYS.length - retryIndex, response: err, }, @@ -174,9 +174,9 @@ const retryWithBackoff = (fn) => { retryIndex++ return Bluebird - .delay(delay) + .delay(delayMs) .then(() => { - debug(`retry #${retryIndex} after ${delay}ms`) + debug(`retry #${retryIndex} after ${delayMs}ms`) return attempt(retryIndex) }) diff --git a/system-tests/__snapshots__/record_spec.js b/system-tests/__snapshots__/record_spec.js index 0571033efc59..eab25bce1aeb 100644 --- a/system-tests/__snapshots__/record_spec.js +++ b/system-tests/__snapshots__/record_spec.js @@ -271,9 +271,9 @@ Please log into Cypress Cloud and find your project. We will list the correct projectId in the 'Settings' tab. -Alternatively, you can create a new project using the Desktop Application. +Alternatively, you can create a new project directly from within the Cypress app. -https://on.cypress.io/dashboard +https://on.cypress.io/cloud ` @@ -332,11 +332,11 @@ exports['e2e record api interaction errors update instance stdout warns but proc (Uploading Results) - Done Uploading (1/1) /foo/bar/.projects/e2e/cypress/screenshots/record_pass.cy.js/yay it passes.png -Warning: We encountered an error talking to our servers. +Warning: We encountered an error communicating with our servers. -This run will not be recorded. +This run will proceed, but will not be recorded. -This error will not alter the exit code. +This error will not affect or change the exit code. StatusCodeError: 500 - "Internal Server Error" @@ -498,7 +498,7 @@ The Record Key is missing. Your CI provider is likely not passing private enviro These results will not be recorded. -This error will not alter the exit code. +This error will not affect or change the exit code. ==================================================================================================== @@ -574,16 +574,7 @@ https://on.cypress.io/run-group-name-not-unique ` exports['e2e record api interaction errors create run unknown 422 errors and exits when there is an unknown 422 response 1'] = ` -We encountered an unexpected error talking to our servers. - -There is likely something wrong with the request. - -The --tag flag you passed was: nightly -The --group flag you passed was: e2e-tests -The --parallel flag you passed was: true -The --ciBuildId flag you passed was: ciBuildId123 - -The server's response was: +We encountered an unexpected error communicating with our servers. StatusCodeError: 422 @@ -592,20 +583,25 @@ StatusCodeError: 422 "message": "An unknown message here from the server." } +There is likely something wrong with the request. + +The --tag flag you passed was: nightly +The --group flag you passed was: e2e-tests +The --parallel flag you passed was: true +The --ciBuildId flag you passed was: ciBuildId123 + ` exports['e2e record api interaction errors create run 500 does not proceed and exits with error when parallelizing 1'] = ` -We encountered an unexpected error talking to our servers. +We encountered an unexpected error communicating with our servers. + +StatusCodeError: 500 - "Internal Server Error" Because you passed the --parallel flag, this run cannot proceed because it requires a valid response from our servers. The --group flag you passed was: foo The --ciBuildId flag you passed was: ciBuildId123 -The server's response was: - -StatusCodeError: 500 - "Internal Server Error" - ` exports['e2e record api interaction errors create instance 500 does not proceed and exits with error when parallelizing and creating instance 1'] = ` @@ -623,17 +619,15 @@ exports['e2e record api interaction errors create instance 500 does not proceed │ Run URL: https://dashboard.cypress.io/projects/cjvoj7/runs/12 │ └────────────────────────────────────────────────────────────────────────────────────────────────┘ -We encountered an unexpected error talking to our servers. +We encountered an unexpected error communicating with our servers. + +StatusCodeError: 500 - "Internal Server Error" Because you passed the --parallel flag, this run cannot proceed because it requires a valid response from our servers. The --group flag you passed was: foo The --ciBuildId flag you passed was: ciBuildId123 -The server's response was: - -StatusCodeError: 500 - "Internal Server Error" - ` exports['e2e record api interaction errors update instance 500 does not proceed and exits with error when parallelizing and updating instance 1'] = ` @@ -690,41 +684,36 @@ exports['e2e record api interaction errors update instance 500 does not proceed (Uploading Results) -We encountered an unexpected error talking to our servers. +We encountered an unexpected error communicating with our servers. + +StatusCodeError: 500 - "Internal Server Error" Because you passed the --parallel flag, this run cannot proceed because it requires a valid response from our servers. The --group flag you passed was: foo The --ciBuildId flag you passed was: ciBuildId123 -The server's response was: - -StatusCodeError: 500 - "Internal Server Error" - ` exports['e2e record api interaction errors api retries on error warns and does not create or update instances 1'] = ` -We encountered an unexpected error talking to our servers. +We encountered an unexpected error communicating with our servers. + +StatusCodeError: 500 - "Internal Server Error" We will retry 3 more times in X second(s)... -The server's response was: +We encountered an unexpected error communicating with our servers. StatusCodeError: 500 - "Internal Server Error" -We encountered an unexpected error talking to our servers. We will retry 2 more times in X second(s)... -The server's response was: +We encountered an unexpected error communicating with our servers. StatusCodeError: 500 - "Internal Server Error" -We encountered an unexpected error talking to our servers. We will retry 1 more time in X second(s)... -The server's response was: - -StatusCodeError: 500 - "Internal Server Error" ==================================================================================================== @@ -739,13 +728,12 @@ StatusCodeError: 500 - "Internal Server Error" │ Run URL: https://dashboard.cypress.io/projects/cjvoj7/runs/12 │ └────────────────────────────────────────────────────────────────────────────────────────────────┘ -We encountered an unexpected error talking to our servers. +We encountered an unexpected error communicating with our servers. -We will retry 3 more times in X second(s)... +StatusCodeError: 500 - "Internal Server Error" -The server's response was: +We will retry 3 more times in X second(s)... -StatusCodeError: 500 - "Internal Server Error" ──────────────────────────────────────────────────────────────────────────────────────────────────── @@ -813,7 +801,7 @@ The Record Key is missing. Your CI provider is likely not passing private enviro These results will not be recorded. -This error will not alter the exit code. +This error will not affect or change the exit code. ==================================================================================================== @@ -894,13 +882,7 @@ https://on.cypress.io/dashboard/organizations/org-id-1234/billing ` exports['e2e record api interaction errors create run 402 - unknown error errors and exits when there\'s an unknown 402 error 1'] = ` -We encountered an unexpected error talking to our servers. - -There is likely something wrong with the request. - -The --tag flag you passed was: - -The server's response was: +We encountered an unexpected error communicating with our servers. StatusCodeError: 402 @@ -908,6 +890,10 @@ StatusCodeError: 402 "error": "Something went wrong" } +There is likely something wrong with the request. + +The --tag flag you passed was: + ` exports['e2e record api interaction errors create run 402 - free plan exceeds monthly tests errors and exits when on free plan and over recorded tests limit 1'] = ` @@ -1147,7 +1133,6 @@ Details: (Screenshots) - - /XXX/XXX/XXX/cypress/screenshots/record_pass.cy.js/yay it passes.png (400x1022) (Uploading Results) @@ -1832,24 +1817,20 @@ https://on.cypress.io/dashboard/organizations/org-id-1234/billing ` exports['e2e record api interaction errors create run 500 errors and exits 1'] = ` -We encountered an unexpected error talking to our servers. - -The server's response was: +We encountered an unexpected error communicating with our servers. StatusCodeError: 500 - "Internal Server Error" ` exports['e2e record api interaction errors create run 500 when grouping without parallelization errors and exits 1'] = ` -We encountered an unexpected error talking to our servers. +We encountered an unexpected error communicating with our servers. + +StatusCodeError: 500 - "Internal Server Error" The --group flag you passed was: foo The --ciBuildId flag you passed was: ciBuildId123 -The server's response was: - -StatusCodeError: 500 - "Internal Server Error" - ` exports['e2e record api interaction errors create instance 500 without parallelization - does not proceed 1'] = ` @@ -1867,9 +1848,7 @@ exports['e2e record api interaction errors create instance 500 without paralleli │ Run URL: https://dashboard.cypress.io/projects/cjvoj7/runs/12 │ └────────────────────────────────────────────────────────────────────────────────────────────────┘ -We encountered an unexpected error talking to our servers. - -The server's response was: +We encountered an unexpected error communicating with our servers. StatusCodeError: 500 - "Internal Server Error" @@ -1890,9 +1869,7 @@ exports['e2e record api interaction errors create instance errors and exits on c │ Run URL: https://dashboard.cypress.io/projects/cjvoj7/runs/12 │ └────────────────────────────────────────────────────────────────────────────────────────────────┘ -We encountered an unexpected error talking to our servers. - -The server's response was: +We encountered an unexpected error communicating with our servers. StatusCodeError: 500 - "Internal Server Error" @@ -1918,15 +1895,13 @@ exports['e2e record api interaction errors postInstanceTests without paralleliza Running: a_record.cy.js (1 of 2) Estimated: X second(s) -We encountered an unexpected error talking to our servers. +We encountered an unexpected error communicating with our servers. + +StatusCodeError: 500 - "Internal Server Error" The --group flag you passed was: foo The --ciBuildId flag you passed was: 1 -The server's response was: - -StatusCodeError: 500 - "Internal Server Error" - ` exports['e2e record api interaction errors postInstanceTests with parallelization errors and exits 1'] = ` @@ -1949,17 +1924,15 @@ exports['e2e record api interaction errors postInstanceTests with parallelizatio Running: a_record.cy.js (1 of 2) Estimated: X second(s) -We encountered an unexpected error talking to our servers. +We encountered an unexpected error communicating with our servers. + +StatusCodeError: 500 - "Internal Server Error" Because you passed the --parallel flag, this run cannot proceed because it requires a valid response from our servers. The --group flag you passed was: foo The --ciBuildId flag you passed was: ciBuildId123 -The server's response was: - -StatusCodeError: 500 - "Internal Server Error" - ` exports['e2e record api interaction errors postInstanceResults errors and exits in serial 1'] = ` @@ -2016,9 +1989,7 @@ exports['e2e record api interaction errors postInstanceResults errors and exits (Uploading Results) -We encountered an unexpected error talking to our servers. - -The server's response was: +We encountered an unexpected error communicating with our servers. StatusCodeError: 500 - "Internal Server Error" @@ -2292,7 +2263,7 @@ exports['e2e record quiet mode respects quiet mode 1'] = ` ` exports['e2e record api interaction errors create run 412 errors and exits when request schema is invalid 1'] = ` -Recording this run failed because the request was invalid. +Recording this run failed. The request was invalid. request should follow postRunRequest@2.0.0 schema From 792ab36e197b24e35c0e66ca0f2ccf5f773af13d Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Thu, 16 Feb 2023 08:29:27 -0500 Subject: [PATCH 14/52] added a bunch of tests covering all error cases except for F3 --- packages/server/lib/cloud/api.ts | 16 +- system-tests/__snapshots__/record_spec.js | 103 +++++- system-tests/test/record_spec.js | 417 ++++++++++++++-------- 3 files changed, 368 insertions(+), 168 deletions(-) diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts index 891de1db98a1..6b7aa35b73ed 100644 --- a/packages/server/lib/cloud/api.ts +++ b/packages/server/lib/cloud/api.ts @@ -91,13 +91,6 @@ const rp = request.defaults((params: CypressRequestOptions, callback) => { params.transform = async function (body, response) { const { statusCode } = response - // if response is invalid or not found - // then bail and do nothing - // and let this error bubble up - if (statusCode >= 500 || statusCode === 404) { - return body - } - // response is valid and we are encrypting if (response.headers['x-cypress-encrypted'] || params.encrypt === 'always') { let decryptedBody @@ -107,6 +100,13 @@ const rp = request.defaults((params: CypressRequestOptions, callback) => { try { decryptedBody = await enc.decryptResponse(body, secretKey) } catch (e) { + // we failed decrypting the response... + + // if status code is >=500 or 404 just return body + if (statusCode >= 500 || statusCode === 404) { + return body + } + throw new DecryptionError(e.message) } @@ -119,6 +119,8 @@ const rp = request.defaults((params: CypressRequestOptions, callback) => { return decryptedBody } + + return body } params.body = jwe diff --git a/system-tests/__snapshots__/record_spec.js b/system-tests/__snapshots__/record_spec.js index eab25bce1aeb..42f633c82d39 100644 --- a/system-tests/__snapshots__/record_spec.js +++ b/system-tests/__snapshots__/record_spec.js @@ -2563,21 +2563,99 @@ Request Sent: ` -exports['e2e record /preflight preflight failure: unencrypted fails on an unencrypted preflight response 1'] = ` -We encountered an unexpected error talking to our servers. +exports['e2e record api interaction errors postPreflight [F1] fails on 500 status codes with empty body after retrying 1'] = ` +We encountered an unexpected error communicating with our servers. + +StatusCodeError: 500 - "Internal Server Error" + +We will retry 1 more time in X second(s)... + +We encountered an unexpected error communicating with our servers. + +StatusCodeError: 500 - "Internal Server Error" Because you passed the --parallel flag, this run cannot proceed because it requires a valid response from our servers. The --group flag you passed was: foo The --ciBuildId flag you passed was: ciBuildId123 -The server's response was: +` + +exports['e2e record api interaction errors postPreflight [F2] fails on 404 status codes with JSON body without retrying 1'] = ` +We could not find a Cypress Cloud project with the projectId: pid123 + +This projectId came from your cypress-with-project-id.config.js file or an environment variable. + +Please log into Cypress Cloud and find your project. + +We will list the correct projectId in the 'Settings' tab. + +Alternatively, you can create a new project directly from within the Cypress app. + +https://on.cypress.io/cloud + +` + +exports['e2e record api interaction errors postPreflight [F2] fails on 404 status codes without JSON body without retrying 1'] = ` +We could not find a Cypress Cloud project with the projectId: pid123 + +This projectId came from your cypress-with-project-id.config.js file or an environment variable. + +Please log into Cypress Cloud and find your project. + +We will list the correct projectId in the 'Settings' tab. + +Alternatively, you can create a new project directly from within the Cypress app. + +https://on.cypress.io/cloud + +` + +exports['e2e record api interaction errors postPreflight [F4] fails on OK status codes with invalid unencrypted data without retrying 1'] = ` +We encountered an unexpected error communicating with our servers. DecryptionError: JWE Recipients missing or incorrect type +Because you passed the --parallel flag, this run cannot proceed because it requires a valid response from our servers. + +The --group flag you passed was: foo +The --ciBuildId flag you passed was: ciBuildId123 + +` + +exports['e2e record api interaction errors postPreflight [F5] fails on OK status codes with empty body without retrying 1'] = ` +We encountered an unexpected error communicating with our servers. + +DecryptionError: General JWE must be an object + +Because you passed the --parallel flag, this run cannot proceed because it requires a valid response from our servers. + +The --group flag you passed was: foo +The --ciBuildId flag you passed was: ciBuildId123 + +` + +exports['e2e record api interaction errors postPreflight preflight failure renders error messages properly 1'] = ` +Recording this run failed. The request was invalid. + +Recording this way is no longer supported + +Errors: + +[ + "attempted to send envUrl foo.bar.baz" +] + +Request Sent: + +{ + "ciBuildId": "ciBuildId123", + "projectId": "cy12345" +} + ` -exports['e2e record /preflight preflight failure: warning message renders preflight warning messages prior to run warnings 1'] = ` +exports['e2e record api interaction errors postPreflight preflight failure: warning message renders preflight warning messages prior to run warnings 1'] = ` Warning from Cypress Cloud: ---------------------------------------------------------------------- @@ -2662,3 +2740,20 @@ https://on.cypress.io/dashboard/organizations/org-id-1234/billing ` + +exports['e2e record api interaction errors postPreflight [F1] fails on request socket errors after retrying 1'] = ` +We encountered an unexpected error communicating with our servers. + +RequestError: Error: socket hang up + +We will retry 1 more time in X second(s)... +We encountered an unexpected error communicating with our servers. + +RequestError: Error: socket hang up + +Because you passed the --parallel flag, this run cannot proceed because it requires a valid response from our servers. + +The --group flag you passed was: foo +The --ciBuildId flag you passed was: ciBuildId123 + +` diff --git a/system-tests/test/record_spec.js b/system-tests/test/record_spec.js index 197eae604057..a4005379ecf9 100644 --- a/system-tests/test/record_spec.js +++ b/system-tests/test/record_spec.js @@ -1265,7 +1265,7 @@ describe('e2e record', () => { }, } })) - it('errors and exits when there\'s an unknown 402 error', function () { + it(`errors and exits when there's an unknown 402 error`, function () { return systemTests.exec(this, { key: 'f858a2bc-b469-4e48-be67-0876339ee7e1', configFile: 'cypress-with-project-id.config.js', @@ -1611,6 +1611,265 @@ describe('e2e record', () => { }) }) }) + + describe('postPreflight', () => { + describe('[F1]', () => { + setupStubbedServer(createRoutes({ + postPreflight: { + res (req, res) { + return req.socket.destroy(new Error('killed')) + }, + }, + })) + + it('fails on request socket errors after retrying', function () { + process.env.API_RETRY_INTERVALS = '1000' + + return systemTests.exec(this, { + key: 'f858a2bc-b469-4e48-be67-0876339ee7e1', + configFile: 'cypress-with-project-id.config.js', + spec: 'record_pass*', + group: 'foo', + tag: 'nightly', + record: true, + parallel: true, + snapshot: true, + ciBuildId: 'ciBuildId123', + expectedExitCode: 1, + }) + }) + }) + + describe('[F1]', () => { + setupStubbedServer(createRoutes({ + postPreflight: { + res (req, res) { + return res.sendStatus(500) + }, + }, + })) + + it('fails on 500 status codes with empty body after retrying', function () { + process.env.API_RETRY_INTERVALS = '1000' + + return systemTests.exec(this, { + key: 'f858a2bc-b469-4e48-be67-0876339ee7e1', + configFile: 'cypress-with-project-id.config.js', + spec: 'record_pass*', + group: 'foo', + tag: 'nightly', + record: true, + parallel: true, + snapshot: true, + ciBuildId: 'ciBuildId123', + expectedExitCode: 1, + }) + }) + }) + + describe('[F2]', () => { + setupStubbedServer(createRoutes({ + postPreflight: { + res (req, res) { + return res + .status(404) + .json({ message: 'not found' }) + }, + }, + })) + + it('fails on 404 status codes with JSON body without retrying', function () { + process.env.API_RETRY_INTERVALS = '1000' + + return systemTests.exec(this, { + key: 'f858a2bc-b469-4e48-be67-0876339ee7e1', + configFile: 'cypress-with-project-id.config.js', + spec: 'record_pass*', + group: 'foo', + tag: 'nightly', + record: true, + parallel: true, + snapshot: true, + ciBuildId: 'ciBuildId123', + expectedExitCode: 1, + }) + }) + }) + + describe('[F2]', () => { + setupStubbedServer(createRoutes({ + postPreflight: { + res (req, res) { + return res.sendStatus(404) + }, + }, + })) + + it('fails on 404 status codes without JSON body without retrying', function () { + process.env.API_RETRY_INTERVALS = '1000' + + return systemTests.exec(this, { + key: 'f858a2bc-b469-4e48-be67-0876339ee7e1', + configFile: 'cypress-with-project-id.config.js', + spec: 'record_pass*', + group: 'foo', + tag: 'nightly', + record: true, + parallel: true, + snapshot: true, + ciBuildId: 'ciBuildId123', + expectedExitCode: 1, + }) + }) + }) + + describe('[F4]', () => { + setupStubbedServer(createRoutes({ + postPreflight: { + res (req, res) { + return res + .status(201) + .json({ data: 'very encrypted and secure string' }) + }, + }, + })) + + it('fails on OK status codes with invalid unencrypted data without retrying', function () { + process.env.API_RETRY_INTERVALS = '1000' + + return systemTests.exec(this, { + key: 'f858a2bc-b469-4e48-be67-0876339ee7e1', + configFile: 'cypress-with-project-id.config.js', + spec: 'record_pass*', + group: 'foo', + tag: 'nightly', + record: true, + parallel: true, + snapshot: true, + ciBuildId: 'ciBuildId123', + expectedExitCode: 1, + }) + }) + }) + + describe('[F5]', () => { + setupStubbedServer(createRoutes({ + postPreflight: { + res (req, res) { + return res.sendStatus(200) + }, + }, + })) + + it('fails on OK status codes with empty body without retrying', function () { + process.env.API_RETRY_INTERVALS = '1000' + + return systemTests.exec(this, { + key: 'f858a2bc-b469-4e48-be67-0876339ee7e1', + configFile: 'cypress-with-project-id.config.js', + spec: 'record_pass*', + group: 'foo', + tag: 'nightly', + record: true, + parallel: true, + snapshot: true, + ciBuildId: 'ciBuildId123', + expectedExitCode: 1, + }) + }) + }) + + describe('preflight failure', () => { + setupStubbedServer(createRoutes({ + postPreflight: { + res: async (req, res) => { + return res.status(412).json(await encryptBody(req, res, { + message: 'Recording this way is no longer supported', + errors: [ + 'attempted to send envUrl foo.bar.baz', + ], + object: { + ciBuildId: 'ciBuildId123', + projectId: 'cy12345', + }, + })) + }, + }, + })) + + it('renders error messages properly', async function () { + return systemTests.exec(this, { + key: 'f858a2bc-b469-4e48-be67-0876339ee7e1', + configFile: 'cypress-with-project-id.config.js', + spec: 'record_pass*', + group: 'foo', + tag: 'nightly', + record: true, + parallel: true, + snapshot: true, + ciBuildId: 'ciBuildId123', + expectedExitCode: 1, + }) + }) + }) + + describe('preflight failure: warning message', () => { + const mockServer = setupStubbedServer(createRoutes({ + postPreflight: { + res: async (req, res) => { + return res.json(await encryptBody(req, res, { + encrypt: true, + apiUrl: req.body.apiUrl, + warnings: [ + { + message: dedent` + ---------------------------------------------------------------------- + This feature will not be supported soon, please check with Cypress to learn more: https://on.cypress.io/ + ---------------------------------------------------------------------- + `, + }, + ], + })) + }, + }, + postRun: { + res (req, res) { + mockServer.setSpecs(req) + + return res.status(200).json({ + runId, + groupId, + machineId, + runUrl, + tags, + warnings: [{ + name: 'foo', + message: 'foo', + code: 'FREE_PLAN_IN_GRACE_PERIOD_EXCEEDS_MONTHLY_PRIVATE_TESTS', + limit: 500, + gracePeriodEnds: '2999-12-31', + orgId: 'org-id-1234', + }], + }) + }, + }, + })) + + it('renders preflight warning messages prior to run warnings', async function () { + return await systemTests.exec(this, { + key: 'f858a2bc-b469-4e48-be67-0876339ee7e1', + configFile: 'cypress-with-project-id.config.js', + spec: 'record_pass*', + group: 'foo', + tag: 'nightly', + record: true, + parallel: true, + snapshot: true, + ciBuildId: 'ciBuildId123', + }) + }) + }) + }) }) describe('api interaction warnings', () => { @@ -1888,160 +2147,4 @@ describe('e2e record', () => { }) }) }) - - describe('/preflight', () => { - describe('preflight failure: unencrypted', () => { - setupStubbedServer(createRoutes({ - preflight: { - res (req, res) { - return res.json({ apiUrl: 'http://localhost:1234' }) - }, - }, - })) - - it('fails on an unencrypted preflight response', async function () { - return systemTests.exec(this, { - key: 'f858a2bc-b469-4e48-be67-0876339ee7e1', - configFile: 'cypress-with-project-id.config.js', - spec: 'record_pass*', - group: 'foo', - tag: 'nightly', - record: true, - parallel: true, - snapshot: true, - ciBuildId: 'ciBuildId123', - expectedExitCode: 1, - }) - }) - }) - - describe('preflight failure 500 server error', () => { - setupStubbedServer(createRoutes({ - preflight: { - res (req, res) { - return res.sendStatus(500) - }, - }, - })) - - it('retries on a preflight server error', async function () { - await new Promise((resolve, reject) => { - let sp - - systemTests.exec(this, { - key: 'f858a2bc-b469-4e48-be67-0876339ee7e1', - configFile: 'cypress-with-project-id.config.js', - spec: 'record_pass*', - group: 'foo', - tag: 'nightly', - record: true, - parallel: true, - ciBuildId: 'ciBuildId123', - onSpawn (spawnResult) { - sp = spawnResult - sp.stdout.on('data', (chunk) => { - const msg = String(chunk) - - if (msg.includes('We will retry')) { - resolve() - sp.kill() - } - }) - }, - }).catch(reject) - }) - }) - }) - - describe('preflight failure', () => { - setupStubbedServer(createRoutes({ - preflight: { - res: async (req, res) => { - return res.status(412).json(await encryptBody(req, res, { - message: 'Recording this way is no longer supported', - errors: [ - 'attempted to send envUrl foo.bar.baz', - ], - object: { - ciBuildId: 'ciBuildId123', - projectId: 'cy12345', - }, - })) - }, - }, - })) - - it('renders error messages properly', async function () { - return systemTests.exec(this, { - key: 'f858a2bc-b469-4e48-be67-0876339ee7e1', - configFile: 'cypress-with-project-id.config.js', - spec: 'record_pass*', - group: 'foo', - tag: 'nightly', - record: true, - parallel: true, - snapshot: true, - ciBuildId: 'ciBuildId123', - expectedExitCode: 1, - }) - }) - }) - - describe('preflight failure: warning message', () => { - const mockServer = setupStubbedServer(createRoutes({ - preflight: { - res: async (req, res) => { - return res.json(await encryptBody(req, res, { - encrypt: true, - apiUrl: req.body.apiUrl, - warnings: [ - { - message: dedent` - ---------------------------------------------------------------------- - This feature will not be supported soon, please check with Cypress to learn more: https://on.cypress.io/ - ---------------------------------------------------------------------- - `, - }, - ], - })) - }, - }, - postRun: { - res (req, res) { - mockServer.setSpecs(req) - - return res.status(200).json({ - runId, - groupId, - machineId, - runUrl, - tags, - warnings: [{ - name: 'foo', - message: 'foo', - code: 'FREE_PLAN_IN_GRACE_PERIOD_EXCEEDS_MONTHLY_PRIVATE_TESTS', - limit: 500, - gracePeriodEnds: '2999-12-31', - orgId: 'org-id-1234', - }], - }) - }, - }, - })) - - it('renders preflight warning messages prior to run warnings', async function () { - return await systemTests.exec(this, { - key: 'f858a2bc-b469-4e48-be67-0876339ee7e1', - configFile: 'cypress-with-project-id.config.js', - spec: 'record_pass*', - group: 'foo', - tag: 'nightly', - record: true, - parallel: true, - snapshot: true, - ciBuildId: 'ciBuildId123', - }) - }) - }) - }) }) From 503e549747c15dcead30d1d61e046e3d8a7f3ab3 Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Thu, 16 Feb 2023 08:44:09 -0500 Subject: [PATCH 15/52] snapshot cleanup and formatting --- packages/errors/src/errors.ts | 3 ++- system-tests/__snapshots__/record_spec.js | 21 +-------------------- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/packages/errors/src/errors.ts b/packages/errors/src/errors.ts index e67874d0e83f..c584a55af819 100644 --- a/packages/errors/src/errors.ts +++ b/packages/errors/src/errors.ts @@ -154,7 +154,8 @@ export const AllCypressErrors = { ${fmt.highlightSecondary(arg1.response)} - We will retry ${fmt.off(arg1.tries)} more ${fmt.off(time)} in ${fmt.off(delay)}...` + We will retry ${fmt.off(arg1.tries)} more ${fmt.off(time)} in ${fmt.off(delay)}... + ` /* Because of fmt.listFlags() and fmt.listItems() */ /* eslint-disable indent */ }, diff --git a/system-tests/__snapshots__/record_spec.js b/system-tests/__snapshots__/record_spec.js index 42f633c82d39..be09e12dc9f8 100644 --- a/system-tests/__snapshots__/record_spec.js +++ b/system-tests/__snapshots__/record_spec.js @@ -2543,26 +2543,6 @@ Available browsers found on your system are: - browser3 ` -exports['e2e record /preflight preflight failure renders error messages properly 1'] = ` -Recording this run failed because the request was invalid. - -Recording this way is no longer supported - -Errors: - -[ - "attempted to send envUrl foo.bar.baz" -] - -Request Sent: - -{ - "ciBuildId": "ciBuildId123", - "projectId": "cy12345" -} - -` - exports['e2e record api interaction errors postPreflight [F1] fails on 500 status codes with empty body after retrying 1'] = ` We encountered an unexpected error communicating with our servers. @@ -2747,6 +2727,7 @@ We encountered an unexpected error communicating with our servers. RequestError: Error: socket hang up We will retry 1 more time in X second(s)... + We encountered an unexpected error communicating with our servers. RequestError: Error: socket hang up From 3e64537da15b5760de0d8e851b99f34b8e710ce8 Mon Sep 17 00:00:00 2001 From: Brian Mann Date: Thu, 16 Feb 2023 08:57:58 -0500 Subject: [PATCH 16/52] update error messages and html snapshots --- .../CANNOT_REMOVE_OLD_BROWSER_PROFILES.html | 2 +- .../errors/__snapshot-html__/CANNOT_TRASH_ASSETS.html | 2 +- .../CLOUD_API_RESPONSE_FAILED_RETRYING - lastTry.html | 9 ++++----- .../CLOUD_API_RESPONSE_FAILED_RETRYING.html | 9 ++++----- .../CLOUD_CANNOT_CREATE_RUN_OR_INSTANCE.html | 6 +++--- .../CLOUD_CANNOT_PROCEED_IN_PARALLEL.html | 10 ++++------ .../CLOUD_CANNOT_PROCEED_IN_SERIAL.html | 10 ++++------ .../__snapshot-html__/CLOUD_CANNOT_UPLOAD_RESULTS.html | 2 +- .../__snapshot-html__/CLOUD_INVALID_RUN_REQUEST.html | 2 +- .../__snapshot-html__/CLOUD_PROJECT_NOT_FOUND.html | 4 ++-- .../CLOUD_UNKNOWN_INVALID_REQUEST.html | 10 ++++------ .../DEV_SERVER_CONFIG_FILE_NOT_FOUND.html | 2 +- .../__snapshot-html__/EXPERIMENTAL_STUDIO_REMOVED.html | 2 +- .../MIGRATION_MISMATCHED_CYPRESS_VERSIONS.html | 2 +- packages/errors/__snapshot-html__/NOT_LOGGED_IN.html | 2 +- .../__snapshot-html__/RECORDING_FROM_FORK_PR.html | 2 +- .../__snapshot-html__/UNEXPECTED_INTERNAL_ERROR.html | 7 ++++--- .../VIDEO_POST_PROCESSING_FAILED.html | 2 +- .../__snapshot-html__/VIDEO_RECORDING_FAILED.html | 2 +- packages/errors/src/errors.ts | 5 +++-- packages/errors/test/unit/visualSnapshotErrors_spec.ts | 4 ++-- packages/server/test/unit/cloud/api_spec.js | 6 +++--- 22 files changed, 48 insertions(+), 54 deletions(-) diff --git a/packages/errors/__snapshot-html__/CANNOT_REMOVE_OLD_BROWSER_PROFILES.html b/packages/errors/__snapshot-html__/CANNOT_REMOVE_OLD_BROWSER_PROFILES.html index 2763657e162e..8beb31f7cf4f 100644 --- a/packages/errors/__snapshot-html__/CANNOT_REMOVE_OLD_BROWSER_PROFILES.html +++ b/packages/errors/__snapshot-html__/CANNOT_REMOVE_OLD_BROWSER_PROFILES.html @@ -36,7 +36,7 @@
Warning: We failed to remove old browser profiles from previous runs.
 
-This error will not alter the exit code.
+This error will not affect or change the exit code.
 
 Error: fail whale
     at makeErr (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
diff --git a/packages/errors/__snapshot-html__/CANNOT_TRASH_ASSETS.html b/packages/errors/__snapshot-html__/CANNOT_TRASH_ASSETS.html
index 3ab26c3ef702..6f2d58c3049e 100644
--- a/packages/errors/__snapshot-html__/CANNOT_TRASH_ASSETS.html
+++ b/packages/errors/__snapshot-html__/CANNOT_TRASH_ASSETS.html
@@ -36,7 +36,7 @@
     
     
Warning: We failed to trash the existing run results.
 
-This error will not alter the exit code.
+This error will not affect or change the exit code.
 
 Error: fail whale
     at makeErr (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
diff --git a/packages/errors/__snapshot-html__/CLOUD_API_RESPONSE_FAILED_RETRYING - lastTry.html b/packages/errors/__snapshot-html__/CLOUD_API_RESPONSE_FAILED_RETRYING - lastTry.html
index 3851fc91ffdb..c93032f73e0a 100644
--- a/packages/errors/__snapshot-html__/CLOUD_API_RESPONSE_FAILED_RETRYING - lastTry.html	
+++ b/packages/errors/__snapshot-html__/CLOUD_API_RESPONSE_FAILED_RETRYING - lastTry.html	
@@ -34,11 +34,10 @@
     
   
     
-    
We encountered an unexpected error talking to our servers.
+    
We encountered an unexpected error communicating with our servers.
 
-We will retry 1 more time in 5 seconds...
-
-The server's response was:
+StatusCodeError: 500 - "Internal Server Error"
 
-StatusCodeError: 500 - "Internal Server Error"
+We will retry 1 more time in 5 seconds...
+
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CLOUD_API_RESPONSE_FAILED_RETRYING.html b/packages/errors/__snapshot-html__/CLOUD_API_RESPONSE_FAILED_RETRYING.html index 8d56bdf9d80d..a8acc2a35f07 100644 --- a/packages/errors/__snapshot-html__/CLOUD_API_RESPONSE_FAILED_RETRYING.html +++ b/packages/errors/__snapshot-html__/CLOUD_API_RESPONSE_FAILED_RETRYING.html @@ -34,11 +34,10 @@ -
We encountered an unexpected error talking to our servers.
+    
We encountered an unexpected error communicating with our servers.
 
-We will retry 3 more times in 5 seconds...
-
-The server's response was:
+StatusCodeError: 500 - "Internal Server Error"
 
-StatusCodeError: 500 - "Internal Server Error"
+We will retry 3 more times in 5 seconds...
+
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CLOUD_CANNOT_CREATE_RUN_OR_INSTANCE.html b/packages/errors/__snapshot-html__/CLOUD_CANNOT_CREATE_RUN_OR_INSTANCE.html index 9f34f7c4ad88..e5cabd1708a6 100644 --- a/packages/errors/__snapshot-html__/CLOUD_CANNOT_CREATE_RUN_OR_INSTANCE.html +++ b/packages/errors/__snapshot-html__/CLOUD_CANNOT_CREATE_RUN_OR_INSTANCE.html @@ -34,11 +34,11 @@ -
Warning: We encountered an error talking to our servers.
+    
Warning: We encountered an error communicating with our servers.
 
-This run will not be recorded.
+This run will proceed, but will not be recorded.
 
-This error will not alter the exit code.
+This error will not affect or change the exit code.
 
 StatusCodeError: 500 - "Internal Server Error"
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CLOUD_CANNOT_PROCEED_IN_PARALLEL.html b/packages/errors/__snapshot-html__/CLOUD_CANNOT_PROCEED_IN_PARALLEL.html index 61a5436cc1c3..4e5ee6d22d0d 100644 --- a/packages/errors/__snapshot-html__/CLOUD_CANNOT_PROCEED_IN_PARALLEL.html +++ b/packages/errors/__snapshot-html__/CLOUD_CANNOT_PROCEED_IN_PARALLEL.html @@ -34,14 +34,12 @@ -
We encountered an unexpected error talking to our servers.
+    
We encountered an unexpected error communicating with our servers.
+
+StatusCodeError: 500 - "Internal Server Error"
 
 Because you passed the --parallel flag, this run cannot proceed because it requires a valid response from our servers.
 
 The --group flag you passed was: foo
-The --ciBuildId flag you passed was: invalid
-
-The server's response was:
-
-StatusCodeError: 500 - "Internal Server Error"
+The --ciBuildId flag you passed was: invalid
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CLOUD_CANNOT_PROCEED_IN_SERIAL.html b/packages/errors/__snapshot-html__/CLOUD_CANNOT_PROCEED_IN_SERIAL.html index ad85fcf206d3..a2843bf81720 100644 --- a/packages/errors/__snapshot-html__/CLOUD_CANNOT_PROCEED_IN_SERIAL.html +++ b/packages/errors/__snapshot-html__/CLOUD_CANNOT_PROCEED_IN_SERIAL.html @@ -34,12 +34,10 @@ -
We encountered an unexpected error talking to our servers.
+    
We encountered an unexpected error communicating with our servers.
 
-The --group flag you passed was: foo
-The --ciBuildId flag you passed was: invalid
-
-The server's response was:
+StatusCodeError: 500 - "Internal Server Error"
 
-StatusCodeError: 500 - "Internal Server Error"
+The --group flag you passed was: foo
+The --ciBuildId flag you passed was: invalid
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CLOUD_CANNOT_UPLOAD_RESULTS.html b/packages/errors/__snapshot-html__/CLOUD_CANNOT_UPLOAD_RESULTS.html index d179c0ff16ea..3068e21d2321 100644 --- a/packages/errors/__snapshot-html__/CLOUD_CANNOT_UPLOAD_RESULTS.html +++ b/packages/errors/__snapshot-html__/CLOUD_CANNOT_UPLOAD_RESULTS.html @@ -38,7 +38,7 @@ These results will not be recorded. -This error will not alter the exit code. +This error will not affect or change the exit code. StatusCodeError: 500 - "Internal Server Error"
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CLOUD_INVALID_RUN_REQUEST.html b/packages/errors/__snapshot-html__/CLOUD_INVALID_RUN_REQUEST.html index d231b57b949f..59221ce2fdb9 100644 --- a/packages/errors/__snapshot-html__/CLOUD_INVALID_RUN_REQUEST.html +++ b/packages/errors/__snapshot-html__/CLOUD_INVALID_RUN_REQUEST.html @@ -34,7 +34,7 @@ -
Recording this run failed because the request was invalid.
+    
Recording this run failed. The request was invalid.
 
 request should follow postRunRequest@2.0.0 schema
 
diff --git a/packages/errors/__snapshot-html__/CLOUD_PROJECT_NOT_FOUND.html b/packages/errors/__snapshot-html__/CLOUD_PROJECT_NOT_FOUND.html
index 9d611931f02d..aa52d1cbda9d 100644
--- a/packages/errors/__snapshot-html__/CLOUD_PROJECT_NOT_FOUND.html
+++ b/packages/errors/__snapshot-html__/CLOUD_PROJECT_NOT_FOUND.html
@@ -42,7 +42,7 @@
 
 We will list the correct projectId in the 'Settings' tab.
 
-Alternatively, you can create a new project using the Desktop Application.
+Alternatively, you can create a new project directly from within the Cypress app.
 
-https://on.cypress.io/dashboard
+https://on.cypress.io/cloud
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CLOUD_UNKNOWN_INVALID_REQUEST.html b/packages/errors/__snapshot-html__/CLOUD_UNKNOWN_INVALID_REQUEST.html index efd9ce64c704..4a8f63595df4 100644 --- a/packages/errors/__snapshot-html__/CLOUD_UNKNOWN_INVALID_REQUEST.html +++ b/packages/errors/__snapshot-html__/CLOUD_UNKNOWN_INVALID_REQUEST.html @@ -34,14 +34,12 @@ -
We encountered an unexpected error talking to our servers.
+    
We encountered an unexpected error communicating with our servers.
+
+StatusCodeError: 500 - "Internal Server Error"
 
 There is likely something wrong with the request.
 
 The --group flag you passed was: foo
-The --ciBuildId flag you passed was: invalid
-
-The server's response was:
-
-StatusCodeError: 500 - "Internal Server Error"
+The --ciBuildId flag you passed was: invalid
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/DEV_SERVER_CONFIG_FILE_NOT_FOUND.html b/packages/errors/__snapshot-html__/DEV_SERVER_CONFIG_FILE_NOT_FOUND.html index d74ab2966357..c1be73f2b254 100644 --- a/packages/errors/__snapshot-html__/DEV_SERVER_CONFIG_FILE_NOT_FOUND.html +++ b/packages/errors/__snapshot-html__/DEV_SERVER_CONFIG_FILE_NOT_FOUND.html @@ -43,7 +43,7 @@ - vite.config.js - vite.config.ts -Add your vite config at one of the above paths, or import your configuration file and provide it to +Add your vite config at one of the above paths, or import your configuration file and provide it to the devServer config as a viteConfig option.
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/EXPERIMENTAL_STUDIO_REMOVED.html b/packages/errors/__snapshot-html__/EXPERIMENTAL_STUDIO_REMOVED.html index 7e7d050d4507..cfc2257dc362 100644 --- a/packages/errors/__snapshot-html__/EXPERIMENTAL_STUDIO_REMOVED.html +++ b/packages/errors/__snapshot-html__/EXPERIMENTAL_STUDIO_REMOVED.html @@ -34,7 +34,7 @@ -
We're ending the experimental phase of Cypress Studio in Cypress version 10.0.0. 
+    
We're ending the experimental phase of Cypress Studio in Cypress version 10.0.0.
 
 If you don't think you can live without Studio or you'd like to learn about how to work around its removal, please join the discussion here: http://on.cypress.io/studio-removal
 
diff --git a/packages/errors/__snapshot-html__/MIGRATION_MISMATCHED_CYPRESS_VERSIONS.html b/packages/errors/__snapshot-html__/MIGRATION_MISMATCHED_CYPRESS_VERSIONS.html
index 138960871add..ce18942189c2 100644
--- a/packages/errors/__snapshot-html__/MIGRATION_MISMATCHED_CYPRESS_VERSIONS.html
+++ b/packages/errors/__snapshot-html__/MIGRATION_MISMATCHED_CYPRESS_VERSIONS.html
@@ -34,7 +34,7 @@
     
   
     
-    
You are running Cypress version 10.0.0 in global mode, but you are attempting to migrate a project where Cypress version 9.6.0 is installed. 
+    
You are running Cypress version 10.0.0 in global mode, but you are attempting to migrate a project where Cypress version 9.6.0 is installed.
 
 Ensure the project you are migrating has Cypress version Cypress version 10.0.0 installed.
 
diff --git a/packages/errors/__snapshot-html__/NOT_LOGGED_IN.html b/packages/errors/__snapshot-html__/NOT_LOGGED_IN.html
index ac98f99b559a..d2b4c94281e8 100644
--- a/packages/errors/__snapshot-html__/NOT_LOGGED_IN.html
+++ b/packages/errors/__snapshot-html__/NOT_LOGGED_IN.html
@@ -36,5 +36,5 @@
     
     
You're not logged in.
 
-Run cypress open to open the Desktop App and log in.
+Run cypress open to open Cypress and log in.
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/RECORDING_FROM_FORK_PR.html b/packages/errors/__snapshot-html__/RECORDING_FROM_FORK_PR.html index 9b5793382784..57ad8f4c20db 100644 --- a/packages/errors/__snapshot-html__/RECORDING_FROM_FORK_PR.html +++ b/packages/errors/__snapshot-html__/RECORDING_FROM_FORK_PR.html @@ -40,5 +40,5 @@ These results will not be recorded. -This error will not alter the exit code.
+This error will not affect or change the exit code.
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/UNEXPECTED_INTERNAL_ERROR.html b/packages/errors/__snapshot-html__/UNEXPECTED_INTERNAL_ERROR.html index 280a2afc4dc3..58a0474b021a 100644 --- a/packages/errors/__snapshot-html__/UNEXPECTED_INTERNAL_ERROR.html +++ b/packages/errors/__snapshot-html__/UNEXPECTED_INTERNAL_ERROR.html @@ -34,10 +34,11 @@ -
We encountered an unexpected internal error. Please check GitHub or open a new issue 
-if you don't see one already with the details below:
+    
We encountered an unexpected internal error.
+
+Please check GitHub or open a new issue if you don't see one already with the details below:
 
 Error: fail whale
     at makeErr (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
-    at UNEXPECTED_INTERNAL_ERROR (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
+    at UNEXPECTED_INTERNAL_ERROR (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/VIDEO_POST_PROCESSING_FAILED.html b/packages/errors/__snapshot-html__/VIDEO_POST_PROCESSING_FAILED.html index d825c4409dc5..5f0017b8e6fb 100644 --- a/packages/errors/__snapshot-html__/VIDEO_POST_PROCESSING_FAILED.html +++ b/packages/errors/__snapshot-html__/VIDEO_POST_PROCESSING_FAILED.html @@ -36,7 +36,7 @@
Warning: We failed processing this video.
 
-This error will not alter the exit code.
+This error will not affect or change the exit code.
 
 Error: fail whale
     at makeErr (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
diff --git a/packages/errors/__snapshot-html__/VIDEO_RECORDING_FAILED.html b/packages/errors/__snapshot-html__/VIDEO_RECORDING_FAILED.html
index b227d04f3322..08df4fedd5b7 100644
--- a/packages/errors/__snapshot-html__/VIDEO_RECORDING_FAILED.html
+++ b/packages/errors/__snapshot-html__/VIDEO_RECORDING_FAILED.html
@@ -36,7 +36,7 @@
     
     
Warning: We failed to record the video.
 
-This error will not alter the exit code.
+This error will not affect or change the exit code.
 
 Error: fail whale
     at makeErr (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
diff --git a/packages/errors/src/errors.ts b/packages/errors/src/errors.ts
index c584a55af819..c9db803324f2 100644
--- a/packages/errors/src/errors.ts
+++ b/packages/errors/src/errors.ts
@@ -1559,8 +1559,9 @@ export const AllCypressErrors = {
 
   UNEXPECTED_INTERNAL_ERROR: (err: Error) => {
     return errTemplate`
-      We encountered an unexpected internal error. Please check GitHub or open a new issue
-      if you don't see one already with the details below:
+      We encountered an unexpected internal error.
+
+      Please check GitHub or open a new issue if you don't see one already with the details below:
 
       ${fmt.stackTrace(err)}
     `
diff --git a/packages/errors/test/unit/visualSnapshotErrors_spec.ts b/packages/errors/test/unit/visualSnapshotErrors_spec.ts
index 8dd2e3bfeed7..2f078cfba3fa 100644
--- a/packages/errors/test/unit/visualSnapshotErrors_spec.ts
+++ b/packages/errors/test/unit/visualSnapshotErrors_spec.ts
@@ -375,12 +375,12 @@ describe('visual error templates', () => {
       return {
         default: [{
           tries: 3,
-          delay: 5000,
+          delayMs: 5000,
           response: makeApiErr(),
         }],
         lastTry: [{
           tries: 1,
-          delay: 5000,
+          delayMs: 5000,
           response: makeApiErr(),
         }],
       }
diff --git a/packages/server/test/unit/cloud/api_spec.js b/packages/server/test/unit/cloud/api_spec.js
index 2b15e267edaa..837cf636f1b3 100644
--- a/packages/server/test/unit/cloud/api_spec.js
+++ b/packages/server/test/unit/cloud/api_spec.js
@@ -1295,19 +1295,19 @@ describe('lib/cloud/api', () => {
         expect(errors.warning).to.be.calledThrice
         expect(errors.warning.firstCall.args[0]).to.eql('CLOUD_API_RESPONSE_FAILED_RETRYING')
         expect(errors.warning.firstCall.args[1]).to.eql({
-          delay: 30000,
+          delayMs: 30000,
           tries: 3,
           response: err,
         })
 
         expect(errors.warning.secondCall.args[1]).to.eql({
-          delay: 60000,
+          delayMs: 60000,
           tries: 2,
           response: err,
         })
 
         expect(errors.warning.thirdCall.args[1]).to.eql({
-          delay: 120000,
+          delayMs: 120000,
           tries: 1,
           response: err,
         })

From f45cb3b802830bc1c82cbbd96e5604bb2697b676 Mon Sep 17 00:00:00 2001
From: Brian Mann 
Date: Thu, 16 Feb 2023 08:58:19 -0500
Subject: [PATCH 17/52] system test function name

---
 system-tests/lib/serverStub.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/system-tests/lib/serverStub.ts b/system-tests/lib/serverStub.ts
index bad85e367d3d..9d3cbc0d425c 100644
--- a/system-tests/lib/serverStub.ts
+++ b/system-tests/lib/serverStub.ts
@@ -70,7 +70,7 @@ export const encryptBody = async (req, res, body) => {
 }
 
 export const routeHandlers = {
-  preflight: {
+  postPreflight: {
     method: 'post',
     url: '/preflight',
     res: async (req, res) => {

From 8ca872420ccb4b2de7555a34da9818e0aab708b8 Mon Sep 17 00:00:00 2001
From: Brian Mann 
Date: Thu, 16 Feb 2023 10:05:38 -0500
Subject: [PATCH 18/52] added tests for F3 and F4 failure states, handle 412
 and 422 status codes

---
 packages/server/lib/cloud/api.ts            |  2 +-
 packages/server/test/unit/cloud/api_spec.js |  4 +-
 system-tests/__snapshots__/record_spec.js   | 29 +++++++--
 system-tests/test/record_spec.js            | 70 +++++++++++++++------
 4 files changed, 76 insertions(+), 29 deletions(-)

diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts
index 6b7aa35b73ed..99bdf64b0b72 100644
--- a/packages/server/lib/cloud/api.ts
+++ b/packages/server/lib/cloud/api.ts
@@ -103,7 +103,7 @@ const rp = request.defaults((params: CypressRequestOptions, callback) => {
             // we failed decrypting the response...
 
             // if status code is >=500 or 404 just return body
-            if (statusCode >= 500 || statusCode === 404) {
+            if (statusCode >= 500 || statusCode === 404 || statusCode === 422) {
               return body
             }
 
diff --git a/packages/server/test/unit/cloud/api_spec.js b/packages/server/test/unit/cloud/api_spec.js
index 837cf636f1b3..0f824b9ca72a 100644
--- a/packages/server/test/unit/cloud/api_spec.js
+++ b/packages/server/test/unit/cloud/api_spec.js
@@ -409,7 +409,7 @@ describe('lib/cloud/api', () => {
 
       })
 
-      it('[F4] POST /preflight statusCode OK but decrypt error', () => {
+      it('[F5] POST /preflight statusCode OK but decrypt error', () => {
         const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
         .reply(200, { data: 'very encrypted and secure string' })
 
@@ -432,7 +432,7 @@ describe('lib/cloud/api', () => {
         })
       })
 
-      it('[F5] POST /preflight statusCode OK but no body', () => {
+      it('[F6] POST /preflight statusCode OK but no body', () => {
         const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
         .reply(200)
 
diff --git a/system-tests/__snapshots__/record_spec.js b/system-tests/__snapshots__/record_spec.js
index be09e12dc9f8..ab67202a8927 100644
--- a/system-tests/__snapshots__/record_spec.js
+++ b/system-tests/__snapshots__/record_spec.js
@@ -2591,7 +2591,7 @@ https://on.cypress.io/cloud
 
 `
 
-exports['e2e record api interaction errors postPreflight [F4] fails on OK status codes with invalid unencrypted data without retrying 1'] = `
+exports['e2e record api interaction errors postPreflight [F5] fails on OK status codes with invalid unencrypted data without retrying 1'] = `
 We encountered an unexpected error communicating with our servers.
 
 DecryptionError: JWE Recipients missing or incorrect type
@@ -2603,7 +2603,7 @@ The --ciBuildId flag you passed was: ciBuildId123
 
 `
 
-exports['e2e record api interaction errors postPreflight [F5] fails on OK status codes with empty body without retrying 1'] = `
+exports['e2e record api interaction errors postPreflight [F6] fails on OK status codes with empty body without retrying 1'] = `
 We encountered an unexpected error communicating with our servers.
 
 DecryptionError: General JWE must be an object
@@ -2615,21 +2615,20 @@ The --ciBuildId flag you passed was: ciBuildId123
 
 `
 
-exports['e2e record api interaction errors postPreflight preflight failure renders error messages properly 1'] = `
+exports['e2e record api interaction errors postPreflight [F3] fails on 412 status codes when request is invalid 1'] = `
 Recording this run failed. The request was invalid.
 
-Recording this way is no longer supported
+Recording is not working
 
 Errors:
 
 [
-  "attempted to send envUrl foo.bar.baz"
+  "attempted to send invalid data"
 ]
 
 Request Sent:
 
 {
-  "ciBuildId": "ciBuildId123",
   "projectId": "cy12345"
 }
 
@@ -2738,3 +2737,21 @@ The --group flag you passed was: foo
 The --ciBuildId flag you passed was: ciBuildId123
 
 `
+
+exports['e2e record api interaction errors postPreflight [F4] fails on 422 status codes even when encryption is off 1'] = `
+We encountered an unexpected error communicating with our servers.
+
+StatusCodeError: 422
+
+{
+  "message": "something broke"
+}
+
+There is likely something wrong with the request.
+
+The --tag flag you passed was: nightly
+The --group flag you passed was: foo
+The --parallel flag you passed was: true
+The --ciBuildId flag you passed was: ciBuildId123
+
+`
diff --git a/system-tests/test/record_spec.js b/system-tests/test/record_spec.js
index a4005379ecf9..5f2cebb9a609 100644
--- a/system-tests/test/record_spec.js
+++ b/system-tests/test/record_spec.js
@@ -1723,18 +1723,53 @@ describe('e2e record', () => {
         })
       })
 
+      describe('[F3]', () => {
+        setupStubbedServer(createRoutes({
+          postPreflight: {
+            res: async (req, res) => {
+              return res.status(412).json(await encryptBody(req, res, {
+                message: 'Recording is not working',
+                errors: [
+                  'attempted to send invalid data',
+                ],
+                object: {
+                  projectId: 'cy12345',
+                },
+              }))
+            },
+          },
+        }))
+
+        it('fails on 412 status codes when request is invalid', function () {
+          process.env.API_RETRY_INTERVALS = '1000'
+
+          return systemTests.exec(this, {
+            key: 'f858a2bc-b469-4e48-be67-0876339ee7e1',
+            configFile: 'cypress-with-project-id.config.js',
+            spec: 'record_pass*',
+            group: 'foo',
+            tag: 'nightly',
+            record: true,
+            parallel: true,
+            snapshot: true,
+            ciBuildId: 'ciBuildId123',
+            expectedExitCode: 1,
+          })
+        })
+      })
+
       describe('[F4]', () => {
         setupStubbedServer(createRoutes({
           postPreflight: {
-            res (req, res) {
-              return res
-              .status(201)
-              .json({ data: 'very encrypted and secure string' })
+            res: async (req, res) => {
+              return res.status(422).json({
+                message: 'something broke',
+              })
             },
           },
         }))
 
-        it('fails on OK status codes with invalid unencrypted data without retrying', function () {
+        it('fails on 422 status codes even when encryption is off', function () {
           process.env.API_RETRY_INTERVALS = '1000'
 
           return systemTests.exec(this, {
@@ -1756,12 +1791,14 @@ describe('e2e record', () => {
         setupStubbedServer(createRoutes({
           postPreflight: {
             res (req, res) {
-              return res.sendStatus(200)
+              return res
+              .status(201)
+              .json({ data: 'very encrypted and secure string' })
             },
           },
         }))
 
-        it('fails on OK status codes with empty body without retrying', function () {
+        it('fails on OK status codes with invalid unencrypted data without retrying', function () {
           process.env.API_RETRY_INTERVALS = '1000'
 
           return systemTests.exec(this, {
@@ -1779,25 +1816,18 @@ describe('e2e record', () => {
         })
       })
 
-      describe('preflight failure', () => {
+      describe('[F6]', () => {
         setupStubbedServer(createRoutes({
           postPreflight: {
-            res: async (req, res) => {
-              return res.status(412).json(await encryptBody(req, res, {
-                message: 'Recording this way is no longer supported',
-                errors: [
-                  'attempted to send envUrl foo.bar.baz',
-                ],
-                object: {
-                  ciBuildId: 'ciBuildId123',
-                  projectId: 'cy12345',
-                },
-              }))
+            res (req, res) {
+              return res.sendStatus(200)
             },
           },
         }))
 
-        it('renders error messages properly', async function () {
+        it('fails on OK status codes with empty body without retrying', function () {
+          process.env.API_RETRY_INTERVALS = '1000'
+
           return systemTests.exec(this, {
             key: 'f858a2bc-b469-4e48-be67-0876339ee7e1',
             configFile: 'cypress-with-project-id.config.js',

From d9142a6440532e1ce71badd6569f6f8a6399c523 Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Thu, 16 Feb 2023 11:18:28 -0600
Subject: [PATCH 19/52] env url resolution

---
 packages/server/lib/cloud/api.ts            | 31 ++++++++++++-
 packages/server/test/unit/cloud/api_spec.js | 50 +++++++++++++++++++++
 scripts/after-pack-hook.js                  | 24 +++++-----
 scripts/binary/binary-sources.js            | 46 +++++++++++++++++++
 4 files changed, 137 insertions(+), 14 deletions(-)

diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts
index 99bdf64b0b72..2e9f901754ef 100644
--- a/packages/server/lib/cloud/api.ts
+++ b/packages/server/lib/cloud/api.ts
@@ -15,6 +15,7 @@ const { apiUrl, apiRoutes, makeRoutes } = require('./routes')
 import Bluebird from 'bluebird'
 import type { OptionsWithUrl } from 'request-promise'
 import * as enc from './encryption'
+import base64Url from 'base64url'
 
 const THIRTY_SECONDS = humanInterval('30 seconds')
 const SIXTY_SECONDS = humanInterval('60 seconds')
@@ -473,12 +474,40 @@ module.exports = {
 
       const preflightBaseProxy = apiUrl.replace('api', 'api-proxy')
 
+      const detectUrl = () => {
+        let envUrls = process.env.CYPRESS_ENV_URLS
+
+        if (envUrls) {
+          const pkgToUrls = JSON.parse(base64Url.decode(envUrls))
+
+          const key = Object.keys(pkgToUrls).find((key) => {
+            try {
+              require.resolve(key, { paths: [process.cwd()] })
+
+              return true
+            } catch (err) {
+              return false
+            }
+          })
+
+          if (key) {
+            return pkgToUrls[key]
+          }
+        }
+
+        return undefined
+      }
+
+      const getEnvUrl = () => {
+        return process.env.CYPRESS_ENV_URL || detectUrl()
+      }
+
       const makeReq = (baseUrl) => {
         return rp.post({
           url: `${baseUrl}preflight`,
           body: {
             apiUrl,
-            envUrl: process.env.CYPRESS_API_URL,
+            envUrl: getEnvUrl(),
             ...preflightInfo,
           },
           headers: {
diff --git a/packages/server/test/unit/cloud/api_spec.js b/packages/server/test/unit/cloud/api_spec.js
index 0f824b9ca72a..9b22f94dea20 100644
--- a/packages/server/test/unit/cloud/api_spec.js
+++ b/packages/server/test/unit/cloud/api_spec.js
@@ -18,6 +18,7 @@ const cache = require('../../../lib/cache')
 const errors = require('../../../lib/errors')
 const machineId = require('../../../lib/cloud/machine_id')
 const Promise = require('bluebird')
+const { default: base64url } = require('base64url')
 
 const API_BASEURL = 'http://localhost:1234'
 const API_PROD_BASEURL = 'https://api.cypress.io'
@@ -258,6 +259,55 @@ describe('lib/cloud/api', () => {
       })
     })
 
+    it('POST /preflight to proxy with detected package. returns encryption', () => {
+      delete process.env.CYPRESS_ENV_URL
+      process.env.CYPRESS_ENV_URLS = base64url.encode(JSON.stringify({
+        'base64url': 'https://base64url.com',
+      }))
+
+      preflightNock(API_PROD_PROXY_BASEURL)
+      .reply(200, decryptReqBodyAndRespond({
+        reqBody: {
+          envUrl: 'https://base64url.com',
+          apiUrl: 'https://api.cypress.io/',
+          projectId: 'abc123',
+        },
+        resBody: {
+          encrypt: true,
+          apiUrl: `${API_PROD_BASEURL}/`,
+        },
+      }))
+
+      return prodApi.postPreflight({ projectId: 'abc123' })
+      .then((ret) => {
+        expect(ret).to.deep.eq({ encrypt: true, apiUrl: `${API_PROD_BASEURL}/` })
+      })
+    })
+
+    it('POST /preflight to proxy with not detected package. returns encryption', () => {
+      delete process.env.CYPRESS_ENV_URL
+      process.env.CYPRESS_ENV_URLS = base64url.encode(JSON.stringify({
+        'weird-package-name-that-will-not-be-found': 'https://weird-package-name-that-will-not-be-found.com',
+      }))
+
+      preflightNock(API_PROD_PROXY_BASEURL)
+      .reply(200, decryptReqBodyAndRespond({
+        reqBody: {
+          apiUrl: 'https://api.cypress.io/',
+          projectId: 'abc123',
+        },
+        resBody: {
+          encrypt: true,
+          apiUrl: `${API_PROD_BASEURL}/`,
+        },
+      }))
+
+      return prodApi.postPreflight({ projectId: 'abc123' })
+      .then((ret) => {
+        expect(ret).to.deep.eq({ encrypt: true, apiUrl: `${API_PROD_BASEURL}/` })
+      })
+    })
+
     it('POST /preflight to proxy, and then api on response status code failure. returns encryption', () => {
       const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
       .reply(500)
diff --git a/scripts/after-pack-hook.js b/scripts/after-pack-hook.js
index cba2426c8f17..62b49f864ef5 100644
--- a/scripts/after-pack-hook.js
+++ b/scripts/after-pack-hook.js
@@ -7,7 +7,7 @@ const path = require('path')
 const { setupV8Snapshots } = require('@tooling/v8-snapshot')
 const { flipFuses, FuseVersion, FuseV1Options } = require('@electron/fuses')
 const { buildEntryPointAndCleanup } = require('./binary/binary-cleanup')
-const { getIntegrityCheckSource, getBinaryEntryPointSource } = require('./binary/binary-sources')
+const { getIntegrityCheckSource, getBinaryEntryPointSource, getEncryptionFileSource, getCloudApiFileSource, validateEncryptionFile } = require('./binary/binary-sources')
 
 module.exports = async function (params) {
   try {
@@ -58,23 +58,21 @@ module.exports = async function (params) {
 
     if (!['1', 'true'].includes(process.env.DISABLE_SNAPSHOT_REQUIRE)) {
       const binaryEntryPointSource = await getBinaryEntryPointSource()
-      const encryptionFile = path.join(outputFolder, 'packages/server/lib/cloud/encryption.js')
-      const fileContents = await fs.readFile(encryptionFile, 'utf8')
-
-      if (!fileContents.includes(`test: CY_TEST,`)) {
-        throw new Error(`Expected to find test key in cloud encryption file`)
-      }
+      const encryptionFilePath = path.join(outputFolder, 'packages/server/lib/cloud/encryption.js')
+      const encryptionFileSource = await getEncryptionFileSource(encryptionFilePath)
+      const cloudApiFilePath = path.join(outputFolder, 'packages/server/lib/cloud/api.js')
+      const cloudApiFileSource = await getCloudApiFileSource(cloudApiFilePath)
 
       await Promise.all([
-        fs.writeFile(encryptionFile, fileContents.replace(`test: CY_TEST,`, '').replace(/const CY_TEST = `(.*?)`/, '')),
+        fs.writeFile(encryptionFilePath, encryptionFileSource),
+        fs.writeFile(cloudApiFilePath, cloudApiFileSource),
         fs.writeFile(path.join(outputFolder, 'index.js'), binaryEntryPointSource),
       ])
 
-      const afterReplace = await fs.readFile(encryptionFile, 'utf8')
-
-      if (afterReplace.includes('CY_TEST')) {
-        throw new Error(`Expected test key to be stripped from cloud encryption file`)
-      }
+      await Promise.all([
+        validateEncryptionFile(encryptionFilePath),
+        validateEncryptionFile(cloudApiFilePath),
+      ])
 
       await flipFuses(
         exePathPerPlatform[os.platform()],
diff --git a/scripts/binary/binary-sources.js b/scripts/binary/binary-sources.js
index e754ca050922..c7fb0111d697 100644
--- a/scripts/binary/binary-sources.js
+++ b/scripts/binary/binary-sources.js
@@ -40,7 +40,53 @@ const getIntegrityCheckSource = (baseDirectory) => {
   .replaceAll('CRYPTO_HMAC_DIGEST_TO_STRING', escapeString(crypto.Hmac.prototype.digest.toString()))
 }
 
+const getEncryptionFileSource = async (encryptionFilePath) => {
+  const fileContents = await fs.readFile(encryptionFilePath, 'utf8')
+
+  if (!fileContents.includes(`test: CY_TEST,`)) {
+    throw new Error(`Expected to find test key in cloud encryption file`)
+  }
+
+  return fileContents.replace(`test: CY_TEST,`, '').replace(/const CY_TEST = `(.*?)`/, '')
+}
+
+const validateEncryptionFile = async (encryptionFilePath) => {
+  const afterReplaceEncryption = await fs.readFile(encryptionFilePath, 'utf8')
+
+  if (afterReplaceEncryption.includes('CY_TEST')) {
+    throw new Error(`Expected test key to be stripped from cloud encryption file`)
+  }
+}
+
+const getCloudApiFileSource = async (cloudApiFilePath) => {
+  const fileContents = await fs.readFile(cloudApiFilePath, 'utf8')
+
+  if (!fileContents.includes('process.env.CYPRESS_ENV_URLS')) {
+    throw new Error(`Expected to find CYPRESS_ENV_URLS in cloud api file`)
+  }
+
+  if (process.env.CYPRESS_ENV_URLS) {
+    return fileContents.replace('process.env.CYPRESS_ENV_URLS', `'${process.env.CYPRESS_ENV_URLS}'`)
+  }
+
+  return fileContents
+}
+
+const validateCloudApiFile = async (cloudApiFilePath) => {
+  if (process.env.CYPRESS_ENV_URLS) {
+    const afterReplaceCloudApi = await fs.readFile(cloudApiFilePath, 'utf8')
+
+    if (afterReplaceCloudApi.includes('process.env.CYPRESS_ENV_URLS')) {
+      throw new Error(`Expected process.env.CYPRESS_ENV_URLS to be stripped from cloud api file`)
+    }
+  }
+}
+
 module.exports = {
   getBinaryEntryPointSource,
   getIntegrityCheckSource,
+  getEncryptionFileSource,
+  getCloudApiFileSource,
+  validateCloudApiFile,
+  validateEncryptionFile,
 }

From 21bf1ab1bc283c6f392616e6799559c7b8b0f078 Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Thu, 16 Feb 2023 12:54:41 -0600
Subject: [PATCH 20/52] fix build issues

---
 scripts/after-pack-hook.js       | 6 ++++--
 scripts/binary/binary-sources.js | 2 +-
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/scripts/after-pack-hook.js b/scripts/after-pack-hook.js
index 62b49f864ef5..2e0a6d461642 100644
--- a/scripts/after-pack-hook.js
+++ b/scripts/after-pack-hook.js
@@ -9,6 +9,8 @@ const { flipFuses, FuseVersion, FuseV1Options } = require('@electron/fuses')
 const { buildEntryPointAndCleanup } = require('./binary/binary-cleanup')
 const { getIntegrityCheckSource, getBinaryEntryPointSource, getEncryptionFileSource, getCloudApiFileSource, validateEncryptionFile } = require('./binary/binary-sources')
 
+const CY_ROOT_DIR = path.join(__dirname, '..')
+
 module.exports = async function (params) {
   try {
     console.log('****************************')
@@ -58,9 +60,9 @@ module.exports = async function (params) {
 
     if (!['1', 'true'].includes(process.env.DISABLE_SNAPSHOT_REQUIRE)) {
       const binaryEntryPointSource = await getBinaryEntryPointSource()
-      const encryptionFilePath = path.join(outputFolder, 'packages/server/lib/cloud/encryption.js')
+      const encryptionFilePath = path.join(CY_ROOT_DIR, 'packages/server/lib/cloud/encryption.ts')
       const encryptionFileSource = await getEncryptionFileSource(encryptionFilePath)
-      const cloudApiFilePath = path.join(outputFolder, 'packages/server/lib/cloud/api.js')
+      const cloudApiFilePath = path.join(CY_ROOT_DIR, 'packages/server/lib/cloud/api.ts')
       const cloudApiFileSource = await getCloudApiFileSource(cloudApiFilePath)
 
       await Promise.all([
diff --git a/scripts/binary/binary-sources.js b/scripts/binary/binary-sources.js
index c7fb0111d697..ef0560b9f9bc 100644
--- a/scripts/binary/binary-sources.js
+++ b/scripts/binary/binary-sources.js
@@ -1,4 +1,4 @@
-const fs = require('fs')
+const fs = require('fs-extra')
 const crypto = require('crypto')
 const path = require('path')
 const esbuild = require('esbuild')

From 2bb00d736f6a47087da2c348b5a58c5a0974a6d4 Mon Sep 17 00:00:00 2001
From: Brian Mann 
Date: Thu, 16 Feb 2023 15:08:16 -0500
Subject: [PATCH 21/52] fix failing tests

---
 packages/server/test/unit/cloud/api_spec.js | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/packages/server/test/unit/cloud/api_spec.js b/packages/server/test/unit/cloud/api_spec.js
index 0f824b9ca72a..a4d7ff9f35cd 100644
--- a/packages/server/test/unit/cloud/api_spec.js
+++ b/packages/server/test/unit/cloud/api_spec.js
@@ -32,6 +32,8 @@ const makeError = (details = {}) => {
   return _.extend(new Error(details.message || 'Some error'), details)
 }
 
+const encryptRequest = encryption.encryptRequest
+
 const decryptReqBodyAndRespond = ({ reqBody, resBody }, fn) => {
   const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
     modulusLength: 2048,
@@ -42,8 +44,6 @@ const decryptReqBodyAndRespond = ({ reqBody, resBody }, fn) => {
    */
   let _secretKey
 
-  const encryptRequest = encryption.encryptRequest
-
   sinon.stub(encryption, 'encryptRequest').callsFake(async (params) => {
     if (reqBody) {
       expect(params.body).to.deep.eq(reqBody)
@@ -51,7 +51,9 @@ const decryptReqBodyAndRespond = ({ reqBody, resBody }, fn) => {
 
     const { secretKey, jwe } = await encryptRequest(params, publicKey)
 
-    encryption.encryptRequest.restore()
+    if (fn) {
+      encryption.encryptRequest.restore()
+    }
 
     _secretKey = secretKey
 

From 0b90a2b68b6cfdb810cf5134d8938af4b37ec6f6 Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Fri, 17 Feb 2023 10:45:47 -0600
Subject: [PATCH 22/52] pr comments

---
 packages/server/lib/cloud/api.ts            | 9 +++++----
 packages/server/lib/modes/record.js         | 4 +++-
 packages/server/test/unit/cloud/api_spec.js | 8 ++++----
 3 files changed, 12 insertions(+), 9 deletions(-)

diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts
index 2e9f901754ef..4d95117ea573 100644
--- a/packages/server/lib/cloud/api.ts
+++ b/packages/server/lib/cloud/api.ts
@@ -225,6 +225,7 @@ const isRetriableError = (err) => {
 }
 
 export type CreateRunOptions = {
+  projectRoot: string
   ci: string
   ciBuildId: string
   projectId: string
@@ -283,7 +284,7 @@ module.exports = {
   },
 
   createRun (options: CreateRunOptions) {
-    const preflightOptions = _.pick(options, ['projectId', 'ciBuildId', 'browser', 'testingType', 'parallel', 'timeout'])
+    const preflightOptions = _.pick(options, ['projectId', 'projectRoot', 'ciBuildId', 'browser', 'testingType', 'parallel', 'timeout'])
 
     return this.postPreflight(preflightOptions)
     .then((result) => {
@@ -468,9 +469,9 @@ module.exports = {
 
   postPreflight (preflightInfo) {
     return retryWithBackoff(async (attemptIndex) => {
-      const { timeout } = preflightInfo
+      const { timeout, projectRoot } = preflightInfo
 
-      preflightInfo = _.omit(preflightInfo, 'timeout')
+      preflightInfo = _.omit(preflightInfo, 'timeout', 'projectRoot')
 
       const preflightBaseProxy = apiUrl.replace('api', 'api-proxy')
 
@@ -482,7 +483,7 @@ module.exports = {
 
           const key = Object.keys(pkgToUrls).find((key) => {
             try {
-              require.resolve(key, { paths: [process.cwd()] })
+              require.resolve(key, { paths: [projectRoot] })
 
               return true
             } catch (err) {
diff --git a/packages/server/lib/modes/record.js b/packages/server/lib/modes/record.js
index c08206113650..0d52e25c7b08 100644
--- a/packages/server/lib/modes/record.js
+++ b/packages/server/lib/modes/record.js
@@ -267,7 +267,7 @@ const createRun = Promise.method((options = {}) => {
     ciBuildId: null,
   })
 
-  let { projectId, recordKey, platform, git, specPattern, specs, parallel, ciBuildId, group, tags, testingType, autoCancelAfterFailures } = options
+  let { projectRoot, projectId, recordKey, platform, git, specPattern, specs, parallel, ciBuildId, group, tags, testingType, autoCancelAfterFailures } = options
 
   if (recordKey == null) {
     recordKey = env.get('CYPRESS_RECORD_KEY')
@@ -310,6 +310,7 @@ const createRun = Promise.method((options = {}) => {
   debugCiInfo('CI provider information %o', ci)
 
   return api.createRun({
+    projectRoot,
     specs,
     group,
     tags,
@@ -617,6 +618,7 @@ const createRunAndRecordSpecs = (options = {}) => {
     }
 
     return createRun({
+      projectRoot,
       git,
       specs,
       group,
diff --git a/packages/server/test/unit/cloud/api_spec.js b/packages/server/test/unit/cloud/api_spec.js
index 678d676892ac..28fdf51ed92d 100644
--- a/packages/server/test/unit/cloud/api_spec.js
+++ b/packages/server/test/unit/cloud/api_spec.js
@@ -229,6 +229,8 @@ describe('lib/cloud/api', () => {
       sinon.restore()
       sinon.stub(os, 'platform').returns('linux')
 
+      delete process.env.CYPRESS_ENV_URL
+      delete process.env.CYPRESS_ENV_URLS
       process.env.CYPRESS_CONFIG_ENV = 'production'
       process.env.CYPRESS_API_URL = 'https://some.server.com'
 
@@ -262,7 +264,6 @@ describe('lib/cloud/api', () => {
     })
 
     it('POST /preflight to proxy with detected package. returns encryption', () => {
-      delete process.env.CYPRESS_ENV_URL
       process.env.CYPRESS_ENV_URLS = base64url.encode(JSON.stringify({
         'base64url': 'https://base64url.com',
       }))
@@ -280,14 +281,13 @@ describe('lib/cloud/api', () => {
         },
       }))
 
-      return prodApi.postPreflight({ projectId: 'abc123' })
+      return prodApi.postPreflight({ projectId: 'abc123', projectRoot: process.cwd() })
       .then((ret) => {
         expect(ret).to.deep.eq({ encrypt: true, apiUrl: `${API_PROD_BASEURL}/` })
       })
     })
 
     it('POST /preflight to proxy with not detected package. returns encryption', () => {
-      delete process.env.CYPRESS_ENV_URL
       process.env.CYPRESS_ENV_URLS = base64url.encode(JSON.stringify({
         'weird-package-name-that-will-not-be-found': 'https://weird-package-name-that-will-not-be-found.com',
       }))
@@ -304,7 +304,7 @@ describe('lib/cloud/api', () => {
         },
       }))
 
-      return prodApi.postPreflight({ projectId: 'abc123' })
+      return prodApi.postPreflight({ projectId: 'abc123', projectRoot: process.cwd() })
       .then((ret) => {
         expect(ret).to.deep.eq({ encrypt: true, apiUrl: `${API_PROD_BASEURL}/` })
       })

From 68e715c73730293cc28fa64e09d9fa5ea0195deb Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Mon, 20 Feb 2023 16:51:46 -0600
Subject: [PATCH 23/52] update env detection logic

---
 packages/server/lib/cloud/api.ts            | 82 ++++++++++++++-----
 packages/server/test/unit/cloud/api_spec.js | 88 ++++++++++++++++++---
 2 files changed, 138 insertions(+), 32 deletions(-)

diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts
index 4d95117ea573..c7feaccbf811 100644
--- a/packages/server/lib/cloud/api.ts
+++ b/packages/server/lib/cloud/api.ts
@@ -16,6 +16,8 @@ import Bluebird from 'bluebird'
 import type { OptionsWithUrl } from 'request-promise'
 import * as enc from './encryption'
 import base64Url from 'base64url'
+import fs from 'fs-extra'
+import path from 'path'
 
 const THIRTY_SECONDS = humanInterval('30 seconds')
 const SIXTY_SECONDS = humanInterval('60 seconds')
@@ -467,6 +469,22 @@ module.exports = {
     responseCache = {}
   },
 
+  async detectEnvUrlFromProcessTree () {
+    let error: { name: string, message: string, stack: string } | undefined
+    let envUrl
+
+    try {
+      // TODO: fill in the correct process tree env url detection
+    } catch (err) {
+      error = err
+    }
+
+    return {
+      envUrl,
+      error,
+    }
+  },
+
   postPreflight (preflightInfo) {
     return retryWithBackoff(async (attemptIndex) => {
       const { timeout, projectRoot } = preflightInfo
@@ -475,40 +493,64 @@ module.exports = {
 
       const preflightBaseProxy = apiUrl.replace('api', 'api-proxy')
 
-      const detectUrl = () => {
-        let envUrls = process.env.CYPRESS_ENV_URLS
+      const getEnvInformation = async () => {
+        let dependencies = {}
+        let errors: { dependency: string, name: string, message: string, stack: string }[] = []
+        let envDependenciesVar = process.env.CYPRESS_ENV_DEPENDENCIES
+        let envUrl = process.env.CYPRESS_ENV_URL
 
-        if (envUrls) {
-          const pkgToUrls = JSON.parse(base64Url.decode(envUrls))
+        if (envDependenciesVar) {
+          let checkProcessTree = true
+          const envDependenciesInformation = JSON.parse(base64Url.decode(envDependenciesVar)) as Record
 
-          const key = Object.keys(pkgToUrls).find((key) => {
+          await Promise.all(Object.entries(envDependenciesInformation).map(async ([key, { processTreeRequirement }]) => {
             try {
-              require.resolve(key, { paths: [projectRoot] })
-
-              return true
-            } catch (err) {
-              return false
+              const packagePath = require.resolve(key, { paths: [projectRoot] })
+              const packageVersion = (await fs.readJSON(path.join(packagePath, '..', 'package.json'), 'utf8')).version
+
+              dependencies[key] = {
+                version: packageVersion,
+              }
+            } catch (error) {
+              if (error.code !== 'MODULE_NOT_FOUND') {
+                errors.push({
+                  dependency: key,
+                  name: error.name,
+                  message: error.message,
+                  stack: error.stack,
+                })
+              }
             }
-          })
+            if (processTreeRequirement === 'presence required' && !dependencies[key]) {
+              checkProcessTree = false
+            } else if (processTreeRequirement === 'absence required' && dependencies[key]) {
+              checkProcessTree = false
+            }
+          }))
+
+          if (!envUrl && checkProcessTree) {
+            const { envUrl: processTreeEnvUrl, error } = await this.detectEnvUrlFromProcessTree()
 
-          if (key) {
-            return pkgToUrls[key]
+            envUrl = processTreeEnvUrl
+            if (error) {
+              errors.push(error)
+            }
           }
         }
 
-        return undefined
-      }
-
-      const getEnvUrl = () => {
-        return process.env.CYPRESS_ENV_URL || detectUrl()
+        return {
+          envUrl,
+          errors: errors.length > 0 ? errors : undefined,
+          dependencies: Object.keys(dependencies).length > 0 ? dependencies : undefined,
+        }
       }
 
-      const makeReq = (baseUrl) => {
+      const makeReq = async (baseUrl) => {
         return rp.post({
           url: `${baseUrl}preflight`,
           body: {
             apiUrl,
-            envUrl: getEnvUrl(),
+            ...await getEnvInformation(),
             ...preflightInfo,
           },
           headers: {
diff --git a/packages/server/test/unit/cloud/api_spec.js b/packages/server/test/unit/cloud/api_spec.js
index 28fdf51ed92d..0be04e1b43b4 100644
--- a/packages/server/test/unit/cloud/api_spec.js
+++ b/packages/server/test/unit/cloud/api_spec.js
@@ -19,6 +19,8 @@ const errors = require('../../../lib/errors')
 const machineId = require('../../../lib/cloud/machine_id')
 const Promise = require('bluebird')
 const { default: base64url } = require('base64url')
+const fs = require('fs-extra')
+const path = require('path')
 
 const API_BASEURL = 'http://localhost:1234'
 const API_PROD_BASEURL = 'https://api.cypress.io'
@@ -229,8 +231,7 @@ describe('lib/cloud/api', () => {
       sinon.restore()
       sinon.stub(os, 'platform').returns('linux')
 
-      delete process.env.CYPRESS_ENV_URL
-      delete process.env.CYPRESS_ENV_URLS
+      process.env.CYPRESS_ENV_URL = 'https://some.server.com'
       process.env.CYPRESS_CONFIG_ENV = 'production'
       process.env.CYPRESS_API_URL = 'https://some.server.com'
 
@@ -247,7 +248,7 @@ describe('lib/cloud/api', () => {
       preflightNock(API_PROD_PROXY_BASEURL)
       .reply(200, decryptReqBodyAndRespond({
         reqBody: {
-          envUrl: 'https://some.server.com', // TODO: fix this
+          envUrl: 'https://some.server.com',
           apiUrl: 'https://api.cypress.io/',
           projectId: 'abc123',
         },
@@ -263,16 +264,32 @@ describe('lib/cloud/api', () => {
       })
     })
 
-    it('POST /preflight to proxy with detected package. returns encryption', () => {
-      process.env.CYPRESS_ENV_URLS = base64url.encode(JSON.stringify({
-        'base64url': 'https://base64url.com',
+    it('POST /preflight to proxy with required packages and no cypress env url. returns encryption', () => {
+      delete process.env.CYPRESS_ENV_URL
+
+      sinon.stub(prodApi, 'detectEnvUrlFromProcessTree').resolves({
+        envUrl: 'https://some.process-tree-server.com',
+      })
+
+      process.env.CYPRESS_ENV_DEPENDENCIES = base64url.encode(JSON.stringify({
+        'base64url': {
+          processTreeRequirement: 'presence required',
+        },
+        'package-that-really-does-not-exist': {
+          processTreeRequirement: 'absence required',
+        },
       }))
 
       preflightNock(API_PROD_PROXY_BASEURL)
       .reply(200, decryptReqBodyAndRespond({
         reqBody: {
-          envUrl: 'https://base64url.com',
           apiUrl: 'https://api.cypress.io/',
+          envUrl: 'https://some.process-tree-server.com',
+          dependencies: {
+            base64url: {
+              version: fs.readJsonSync(path.join(require.resolve('base64url', { paths: [process.cwd()] }), '..', 'package.json'), 'utf8').version,
+            },
+          },
           projectId: 'abc123',
         },
         resBody: {
@@ -284,12 +301,58 @@ describe('lib/cloud/api', () => {
       return prodApi.postPreflight({ projectId: 'abc123', projectRoot: process.cwd() })
       .then((ret) => {
         expect(ret).to.deep.eq({ encrypt: true, apiUrl: `${API_PROD_BASEURL}/` })
+        expect(prodApi.detectEnvUrlFromProcessTree).to.be.calledOnce
       })
     })
 
-    it('POST /preflight to proxy with not detected package. returns encryption', () => {
-      process.env.CYPRESS_ENV_URLS = base64url.encode(JSON.stringify({
-        'weird-package-name-that-will-not-be-found': 'https://weird-package-name-that-will-not-be-found.com',
+    it('POST /preflight to proxy without required absent packages and no cypress env url. returns encryption', () => {
+      delete process.env.CYPRESS_ENV_URL
+
+      sinon.stub(prodApi, 'detectEnvUrlFromProcessTree').resolves({
+        envUrl: 'https://some.process-tree-server.com',
+      })
+
+      process.env.CYPRESS_ENV_DEPENDENCIES = base64url.encode(JSON.stringify({
+        'base64url': {
+          processTreeRequirement: 'absence required',
+        },
+      }))
+
+      preflightNock(API_PROD_PROXY_BASEURL)
+      .reply(200, decryptReqBodyAndRespond({
+        reqBody: {
+          apiUrl: 'https://api.cypress.io/',
+          dependencies: {
+            base64url: {
+              version: fs.readJsonSync(path.join(require.resolve('base64url', { paths: [process.cwd()] }), '..', 'package.json'), 'utf8').version,
+            },
+          },
+          projectId: 'abc123',
+        },
+        resBody: {
+          encrypt: true,
+          apiUrl: `${API_PROD_BASEURL}/`,
+        },
+      }))
+
+      return prodApi.postPreflight({ projectId: 'abc123', projectRoot: process.cwd() })
+      .then((ret) => {
+        expect(ret).to.deep.eq({ encrypt: true, apiUrl: `${API_PROD_BASEURL}/` })
+        expect(prodApi.detectEnvUrlFromProcessTree).not.to.be.called
+      })
+    })
+
+    it('POST /preflight to proxy without required present packages and no cypress env url. returns encryption', () => {
+      delete process.env.CYPRESS_ENV_URL
+
+      sinon.stub(prodApi, 'detectEnvUrlFromProcessTree').resolves({
+        envUrl: 'https://some.process-tree-server.com',
+      })
+
+      process.env.CYPRESS_ENV_DEPENDENCIES = base64url.encode(JSON.stringify({
+        'package-that-really-does-not-exist': {
+          processTreeRequirement: 'presence required',
+        },
       }))
 
       preflightNock(API_PROD_PROXY_BASEURL)
@@ -307,6 +370,7 @@ describe('lib/cloud/api', () => {
       return prodApi.postPreflight({ projectId: 'abc123', projectRoot: process.cwd() })
       .then((ret) => {
         expect(ret).to.deep.eq({ encrypt: true, apiUrl: `${API_PROD_BASEURL}/` })
+        expect(prodApi.detectEnvUrlFromProcessTree).not.to.be.called
       })
     })
 
@@ -317,7 +381,7 @@ describe('lib/cloud/api', () => {
       const scopeApi = preflightNock(API_PROD_BASEURL)
       .reply(200, decryptReqBodyAndRespond({
         reqBody: {
-          envUrl: 'https://some.server.com', // TODO: fix this
+          envUrl: 'https://some.server.com',
           apiUrl: 'https://api.cypress.io/',
           projectId: 'abc123',
         },
@@ -342,7 +406,7 @@ describe('lib/cloud/api', () => {
       const scopeApi = preflightNock(API_PROD_BASEURL)
       .reply(200, decryptReqBodyAndRespond({
         reqBody: {
-          envUrl: 'https://some.server.com', // TODO: fix this
+          envUrl: 'https://some.server.com',
           apiUrl: 'https://api.cypress.io/',
           projectId: 'abc123',
         },

From 26353d9f4f955c6b11b1f3763fba24ee7377eaae Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Wed, 22 Feb 2023 13:21:26 -0600
Subject: [PATCH 24/52] refactor environment detection and add tests

---
 packages/server/lib/cloud/api.ts              |  81 +---------
 packages/server/lib/cloud/environment.ts      | 117 +++++++++++++++
 packages/server/package.json                  |   1 +
 .../all-tracked-dependencies/package.json     |   8 +
 .../package.json                              |   7 +
 .../package.json                              |   7 +
 .../cloud/environment/test-project/child.js   |  26 ++++
 .../environment/test-project/grandchild.js    |  10 ++
 .../cloud/environment/test-project/index.js   |  26 ++++
 .../environment/test-project/package.json     |   5 +
 packages/server/test/unit/cloud/api_spec.js   | 113 --------------
 .../test/unit/cloud/environment_spec.ts       | 138 ++++++++++++++++++
 yarn.lock                                     |   7 +
 13 files changed, 358 insertions(+), 188 deletions(-)
 create mode 100644 packages/server/lib/cloud/environment.ts
 create mode 100644 packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/package.json
 create mode 100644 packages/server/test/support/fixtures/cloud/environment/partial-dependencies-matching/package.json
 create mode 100644 packages/server/test/support/fixtures/cloud/environment/partial-dependencies-not-matching/package.json
 create mode 100644 packages/server/test/support/fixtures/cloud/environment/test-project/child.js
 create mode 100644 packages/server/test/support/fixtures/cloud/environment/test-project/grandchild.js
 create mode 100644 packages/server/test/support/fixtures/cloud/environment/test-project/index.js
 create mode 100644 packages/server/test/support/fixtures/cloud/environment/test-project/package.json
 create mode 100644 packages/server/test/unit/cloud/environment_spec.ts

diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts
index c7feaccbf811..41d4e4196146 100644
--- a/packages/server/lib/cloud/api.ts
+++ b/packages/server/lib/cloud/api.ts
@@ -15,9 +15,7 @@ const { apiUrl, apiRoutes, makeRoutes } = require('./routes')
 import Bluebird from 'bluebird'
 import type { OptionsWithUrl } from 'request-promise'
 import * as enc from './encryption'
-import base64Url from 'base64url'
-import fs from 'fs-extra'
-import path from 'path'
+import getEnvInformationForProjectRoot from './environment'
 
 const THIRTY_SECONDS = humanInterval('30 seconds')
 const SIXTY_SECONDS = humanInterval('60 seconds')
@@ -469,22 +467,6 @@ module.exports = {
     responseCache = {}
   },
 
-  async detectEnvUrlFromProcessTree () {
-    let error: { name: string, message: string, stack: string } | undefined
-    let envUrl
-
-    try {
-      // TODO: fill in the correct process tree env url detection
-    } catch (err) {
-      error = err
-    }
-
-    return {
-      envUrl,
-      error,
-    }
-  },
-
   postPreflight (preflightInfo) {
     return retryWithBackoff(async (attemptIndex) => {
       const { timeout, projectRoot } = preflightInfo
@@ -493,64 +475,12 @@ module.exports = {
 
       const preflightBaseProxy = apiUrl.replace('api', 'api-proxy')
 
-      const getEnvInformation = async () => {
-        let dependencies = {}
-        let errors: { dependency: string, name: string, message: string, stack: string }[] = []
-        let envDependenciesVar = process.env.CYPRESS_ENV_DEPENDENCIES
-        let envUrl = process.env.CYPRESS_ENV_URL
-
-        if (envDependenciesVar) {
-          let checkProcessTree = true
-          const envDependenciesInformation = JSON.parse(base64Url.decode(envDependenciesVar)) as Record
-
-          await Promise.all(Object.entries(envDependenciesInformation).map(async ([key, { processTreeRequirement }]) => {
-            try {
-              const packagePath = require.resolve(key, { paths: [projectRoot] })
-              const packageVersion = (await fs.readJSON(path.join(packagePath, '..', 'package.json'), 'utf8')).version
-
-              dependencies[key] = {
-                version: packageVersion,
-              }
-            } catch (error) {
-              if (error.code !== 'MODULE_NOT_FOUND') {
-                errors.push({
-                  dependency: key,
-                  name: error.name,
-                  message: error.message,
-                  stack: error.stack,
-                })
-              }
-            }
-            if (processTreeRequirement === 'presence required' && !dependencies[key]) {
-              checkProcessTree = false
-            } else if (processTreeRequirement === 'absence required' && dependencies[key]) {
-              checkProcessTree = false
-            }
-          }))
-
-          if (!envUrl && checkProcessTree) {
-            const { envUrl: processTreeEnvUrl, error } = await this.detectEnvUrlFromProcessTree()
-
-            envUrl = processTreeEnvUrl
-            if (error) {
-              errors.push(error)
-            }
-          }
-        }
-
-        return {
-          envUrl,
-          errors: errors.length > 0 ? errors : undefined,
-          dependencies: Object.keys(dependencies).length > 0 ? dependencies : undefined,
-        }
-      }
-
-      const makeReq = async (baseUrl) => {
+      const makeReq = async (baseUrl, agent) => {
         return rp.post({
           url: `${baseUrl}preflight`,
           body: {
             apiUrl,
-            ...await getEnvInformation(),
+            ...await getEnvInformationForProjectRoot(projectRoot),
             ...preflightInfo,
           },
           headers: {
@@ -560,13 +490,14 @@ module.exports = {
           timeout: timeout ?? SIXTY_SECONDS,
           json: true,
           encrypt: 'always',
+          agent,
         })
       }
 
       const postReqs = async () => {
-        return makeReq(preflightBaseProxy)
+        return makeReq(preflightBaseProxy, null)
         .catch((err) => {
-          return makeReq(apiUrl)
+          return makeReq(apiUrl, agent)
         })
       }
 
diff --git a/packages/server/lib/cloud/environment.ts b/packages/server/lib/cloud/environment.ts
new file mode 100644
index 000000000000..5ffa7ed46989
--- /dev/null
+++ b/packages/server/lib/cloud/environment.ts
@@ -0,0 +1,117 @@
+import { exec } from 'child_process'
+import { promisify } from 'util'
+import base64Url from 'base64url'
+import fs from 'fs-extra'
+import resolvePackagePath from 'resolve-package-path'
+
+const execAsync = promisify(exec)
+
+const getProcessBranchForPid = async (pid: string) => {
+  const { stdout } = await execAsync('ps -eo pid=,ppid=')
+  const processTree = stdout.split('\n').reduce((acc, line) => {
+    const [pid, ppid] = line.trim().split(/\s+/)
+
+    acc[pid] = ppid
+
+    return acc
+  }, {})
+
+  const currentProcessBranch: string[] = []
+
+  while (pid) {
+    currentProcessBranch.push(pid)
+    pid = processTree[pid]
+  }
+
+  return currentProcessBranch
+}
+
+const getCypressEnvUrlFromProcessBranch = async (pid: string) => {
+  let error: { name: string, message: string, stack: string } | undefined
+  let envUrl
+
+  if (process.platform !== 'win32') {
+    try {
+      const processBranch = await getProcessBranchForPid(pid)
+      const { stdout } = await execAsync(`ps eww -p ${processBranch.join(',')} -o pid=,command=`)
+
+      const pidEnvUrlMapping = stdout.split('\n').reduce((acc, line) => {
+        const cypressEnvUrl = line.trim().match(/(\d+).*CYPRESS_ENV_URL=(\S+)/)
+
+        if (cypressEnvUrl) {
+          acc[cypressEnvUrl[1]] = cypressEnvUrl[2]
+        }
+
+        return acc
+      }, {})
+
+      const foundPid = processBranch.find((pid) => pidEnvUrlMapping[pid])
+
+      if (foundPid) {
+        envUrl = pidEnvUrlMapping[foundPid]
+      }
+    } catch (err) {
+      error = err
+    }
+  }
+
+  return {
+    envUrl,
+    error,
+  }
+}
+
+const getEnvInformationForProjectRoot = async (projectRoot: string, pid: string) => {
+  let dependencies = {}
+  let errors: { dependency?: string, name: string, message: string, stack: string }[] = []
+  let envDependenciesVar = process.env.CYPRESS_ENV_DEPENDENCIES
+  let envUrl = process.env.CYPRESS_ENV_URL
+
+  if (envDependenciesVar) {
+    let checkProcessTree = true
+    const envDependenciesInformation = JSON.parse(base64Url.decode(envDependenciesVar)) as Record
+
+    await Promise.all(Object.entries(envDependenciesInformation).map(async ([dependency, { processTreeRequirement }]) => {
+      try {
+        const packageJsonPath = resolvePackagePath(dependency, projectRoot)
+
+        if (packageJsonPath) {
+          const packageVersion = (await fs.readJSON(packageJsonPath)).version
+
+          dependencies[dependency] = {
+            version: packageVersion,
+          }
+        }
+      } catch (error) {
+        errors.push({
+          dependency,
+          name: error.name,
+          message: error.message,
+          stack: error.stack,
+        })
+      }
+      if (processTreeRequirement === 'presence required' && !dependencies[dependency]) {
+        checkProcessTree = false
+      } else if (processTreeRequirement === 'absence required' && dependencies[dependency]) {
+        checkProcessTree = false
+      }
+    }))
+
+    if (!envUrl && checkProcessTree) {
+      const { envUrl: processTreeEnvUrl, error } = await getCypressEnvUrlFromProcessBranch(pid)
+
+      envUrl = processTreeEnvUrl
+      if (error) {
+        errors.push(error)
+      }
+    }
+  }
+
+  return {
+    ...(envUrl ? { envUrl } : {}),
+    ...(errors.length > 0 ? { errors } : {}),
+    ...(Object.keys(dependencies).length > 0 ? { dependencies } : {}),
+  }
+}
+
+export default getEnvInformationForProjectRoot
diff --git a/packages/server/package.json b/packages/server/package.json
index 59d1c07826f2..a2192e40a130 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -107,6 +107,7 @@
     "randomstring": "1.1.5",
     "recast": "0.20.4",
     "resolve": "1.17.0",
+    "resolve-package-path": "4.0.3",
     "sanitize-filename": "1.6.3",
     "semver": "7.3.2",
     "send": "0.17.1",
diff --git a/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/package.json b/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/package.json
new file mode 100644
index 000000000000..18a31aa405f5
--- /dev/null
+++ b/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/package.json
@@ -0,0 +1,8 @@
+{
+  "name": "all-tracked-dependencies",
+  "version": "1.0.0",
+  "dependencies": {
+    "bar": "2.0.0",
+    "foo": "1.0.0"
+  }
+}
diff --git a/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-matching/package.json b/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-matching/package.json
new file mode 100644
index 000000000000..902097307b95
--- /dev/null
+++ b/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-matching/package.json
@@ -0,0 +1,7 @@
+{
+  "name": "all-tracked-dependencies",
+  "version": "1.0.0",
+  "dependencies": {
+    "foo": "1.0.0"
+  }
+}
diff --git a/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-not-matching/package.json b/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-not-matching/package.json
new file mode 100644
index 000000000000..ddbbee04ac36
--- /dev/null
+++ b/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-not-matching/package.json
@@ -0,0 +1,7 @@
+{
+  "name": "all-tracked-dependencies",
+  "version": "1.0.0",
+  "dependencies": {
+    "bar": "2.0.0"
+  }
+}
diff --git a/packages/server/test/support/fixtures/cloud/environment/test-project/child.js b/packages/server/test/support/fixtures/cloud/environment/test-project/child.js
new file mode 100644
index 000000000000..901860d9f630
--- /dev/null
+++ b/packages/server/test/support/fixtures/cloud/environment/test-project/child.js
@@ -0,0 +1,26 @@
+import { spawn } from 'child_process'
+import path from 'path'
+import * as url from 'url'
+
+// eslint-disable-next-line no-console
+console.log('child', process.pid, process.ppid, process.env.CHILD_CYPRESS_ENV_URL)
+
+const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
+
+const proc = spawn('node', ['grandchild.js'], {
+  cwd: path.join(__dirname),
+  stdio: 'inherit',
+  env: {
+    ...process.env,
+    CYPRESS_ENV_URL: process.env.PARENT_CYPRESS_ENV_URL,
+  },
+})
+
+const timeout = setTimeout(() => {
+
+}, 1e9)
+
+process.on('SIGTERM', () => {
+  clearTimeout(timeout)
+  proc.kill()
+})
diff --git a/packages/server/test/support/fixtures/cloud/environment/test-project/grandchild.js b/packages/server/test/support/fixtures/cloud/environment/test-project/grandchild.js
new file mode 100644
index 000000000000..6b3a2b15dea2
--- /dev/null
+++ b/packages/server/test/support/fixtures/cloud/environment/test-project/grandchild.js
@@ -0,0 +1,10 @@
+// eslint-disable-next-line no-console
+console.log('grandchild', process.pid, process.ppid, process.env.GRANDCHILD_CYPRESS_ENV_URL)
+
+const timeout = setTimeout(() => {
+
+}, 1e9)
+
+process.on('SIGTERM', () => {
+  clearTimeout(timeout)
+})
diff --git a/packages/server/test/support/fixtures/cloud/environment/test-project/index.js b/packages/server/test/support/fixtures/cloud/environment/test-project/index.js
new file mode 100644
index 000000000000..218f9d248b1c
--- /dev/null
+++ b/packages/server/test/support/fixtures/cloud/environment/test-project/index.js
@@ -0,0 +1,26 @@
+import { spawn } from 'child_process'
+import path from 'path'
+import * as url from 'url'
+
+// eslint-disable-next-line no-console
+console.log('parent', process.pid, process.ppid, process.env.PARENT_CYPRESS_ENV_URL)
+
+const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
+
+const proc = spawn('node', ['child.js'], {
+  cwd: path.join(__dirname),
+  stdio: 'inherit',
+  env: {
+    ...process.env,
+    CYPRESS_ENV_URL: process.env.PARENT_CYPRESS_ENV_URL,
+  },
+})
+
+const timeout = setTimeout(() => {
+
+}, 1e9)
+
+process.on('SIGTERM', () => {
+  clearTimeout(timeout)
+  proc.kill()
+})
diff --git a/packages/server/test/support/fixtures/cloud/environment/test-project/package.json b/packages/server/test/support/fixtures/cloud/environment/test-project/package.json
new file mode 100644
index 000000000000..eb235e08126c
--- /dev/null
+++ b/packages/server/test/support/fixtures/cloud/environment/test-project/package.json
@@ -0,0 +1,5 @@
+{
+  "name": "test-project",
+  "version": "1.0.0",
+  "type": "module"
+}
\ No newline at end of file
diff --git a/packages/server/test/unit/cloud/api_spec.js b/packages/server/test/unit/cloud/api_spec.js
index 0be04e1b43b4..499551a85f60 100644
--- a/packages/server/test/unit/cloud/api_spec.js
+++ b/packages/server/test/unit/cloud/api_spec.js
@@ -18,9 +18,6 @@ const cache = require('../../../lib/cache')
 const errors = require('../../../lib/errors')
 const machineId = require('../../../lib/cloud/machine_id')
 const Promise = require('bluebird')
-const { default: base64url } = require('base64url')
-const fs = require('fs-extra')
-const path = require('path')
 
 const API_BASEURL = 'http://localhost:1234'
 const API_PROD_BASEURL = 'https://api.cypress.io'
@@ -264,116 +261,6 @@ describe('lib/cloud/api', () => {
       })
     })
 
-    it('POST /preflight to proxy with required packages and no cypress env url. returns encryption', () => {
-      delete process.env.CYPRESS_ENV_URL
-
-      sinon.stub(prodApi, 'detectEnvUrlFromProcessTree').resolves({
-        envUrl: 'https://some.process-tree-server.com',
-      })
-
-      process.env.CYPRESS_ENV_DEPENDENCIES = base64url.encode(JSON.stringify({
-        'base64url': {
-          processTreeRequirement: 'presence required',
-        },
-        'package-that-really-does-not-exist': {
-          processTreeRequirement: 'absence required',
-        },
-      }))
-
-      preflightNock(API_PROD_PROXY_BASEURL)
-      .reply(200, decryptReqBodyAndRespond({
-        reqBody: {
-          apiUrl: 'https://api.cypress.io/',
-          envUrl: 'https://some.process-tree-server.com',
-          dependencies: {
-            base64url: {
-              version: fs.readJsonSync(path.join(require.resolve('base64url', { paths: [process.cwd()] }), '..', 'package.json'), 'utf8').version,
-            },
-          },
-          projectId: 'abc123',
-        },
-        resBody: {
-          encrypt: true,
-          apiUrl: `${API_PROD_BASEURL}/`,
-        },
-      }))
-
-      return prodApi.postPreflight({ projectId: 'abc123', projectRoot: process.cwd() })
-      .then((ret) => {
-        expect(ret).to.deep.eq({ encrypt: true, apiUrl: `${API_PROD_BASEURL}/` })
-        expect(prodApi.detectEnvUrlFromProcessTree).to.be.calledOnce
-      })
-    })
-
-    it('POST /preflight to proxy without required absent packages and no cypress env url. returns encryption', () => {
-      delete process.env.CYPRESS_ENV_URL
-
-      sinon.stub(prodApi, 'detectEnvUrlFromProcessTree').resolves({
-        envUrl: 'https://some.process-tree-server.com',
-      })
-
-      process.env.CYPRESS_ENV_DEPENDENCIES = base64url.encode(JSON.stringify({
-        'base64url': {
-          processTreeRequirement: 'absence required',
-        },
-      }))
-
-      preflightNock(API_PROD_PROXY_BASEURL)
-      .reply(200, decryptReqBodyAndRespond({
-        reqBody: {
-          apiUrl: 'https://api.cypress.io/',
-          dependencies: {
-            base64url: {
-              version: fs.readJsonSync(path.join(require.resolve('base64url', { paths: [process.cwd()] }), '..', 'package.json'), 'utf8').version,
-            },
-          },
-          projectId: 'abc123',
-        },
-        resBody: {
-          encrypt: true,
-          apiUrl: `${API_PROD_BASEURL}/`,
-        },
-      }))
-
-      return prodApi.postPreflight({ projectId: 'abc123', projectRoot: process.cwd() })
-      .then((ret) => {
-        expect(ret).to.deep.eq({ encrypt: true, apiUrl: `${API_PROD_BASEURL}/` })
-        expect(prodApi.detectEnvUrlFromProcessTree).not.to.be.called
-      })
-    })
-
-    it('POST /preflight to proxy without required present packages and no cypress env url. returns encryption', () => {
-      delete process.env.CYPRESS_ENV_URL
-
-      sinon.stub(prodApi, 'detectEnvUrlFromProcessTree').resolves({
-        envUrl: 'https://some.process-tree-server.com',
-      })
-
-      process.env.CYPRESS_ENV_DEPENDENCIES = base64url.encode(JSON.stringify({
-        'package-that-really-does-not-exist': {
-          processTreeRequirement: 'presence required',
-        },
-      }))
-
-      preflightNock(API_PROD_PROXY_BASEURL)
-      .reply(200, decryptReqBodyAndRespond({
-        reqBody: {
-          apiUrl: 'https://api.cypress.io/',
-          projectId: 'abc123',
-        },
-        resBody: {
-          encrypt: true,
-          apiUrl: `${API_PROD_BASEURL}/`,
-        },
-      }))
-
-      return prodApi.postPreflight({ projectId: 'abc123', projectRoot: process.cwd() })
-      .then((ret) => {
-        expect(ret).to.deep.eq({ encrypt: true, apiUrl: `${API_PROD_BASEURL}/` })
-        expect(prodApi.detectEnvUrlFromProcessTree).not.to.be.called
-      })
-    })
-
     it('POST /preflight to proxy, and then api on response status code failure. returns encryption', () => {
       const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
       .reply(500)
diff --git a/packages/server/test/unit/cloud/environment_spec.ts b/packages/server/test/unit/cloud/environment_spec.ts
new file mode 100644
index 000000000000..0d61f11a0bfc
--- /dev/null
+++ b/packages/server/test/unit/cloud/environment_spec.ts
@@ -0,0 +1,138 @@
+import '../../spec_helper'
+import getEnvInformationForProjectRoot from '../../../lib/cloud/environment'
+import path from 'path'
+import base64url from 'base64url'
+import { exec } from 'child_process'
+
+describe('lib/cloud/api', () => {
+  beforeEach(() => {
+    delete process.env.CYPRESS_ENV_URL
+    process.env.CYPRESS_ENV_DEPENDENCIES = base64url.encode(JSON.stringify({
+      'foo': {
+        processTreeRequirement: 'presence required',
+      },
+      'bar': {
+        processTreeRequirement: 'absence required',
+      },
+    }))
+  })
+
+  let proc
+  const spawnProcessTree = async ({
+    grandParentUrl,
+    parentUrl,
+    url,
+  }: {
+    grandParentUrl?: string
+    parentUrl?: string
+    url?: string
+  }) => {
+    return new Promise((resolve) => {
+      proc = exec(`node ${path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'test-project', 'index.js')}`, {
+        cwd: path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'test-project'),
+        env: {
+          ...process.env,
+          CYPRESS_ENV_URL: grandParentUrl,
+          CHILD_CYPRESS_ENV_URL: parentUrl,
+          GRANDCHILD_CYPRESS_ENV_URL: url,
+        },
+      })
+
+      proc.stdout.on('data', (data) => {
+        const match = data.toString().match(/grandchild (\d+)/)
+
+        if (match) {
+          resolve(match[1])
+        }
+      })
+    })
+  }
+
+  afterEach(() => {
+    if (proc) {
+      proc.kill()
+    }
+  })
+
+  it('should be able to get the environment for: present CYPRESS_ENV_URL and all tracked dependencies', async () => {
+    process.env.CYPRESS_ENV_URL = 'https://example.com'
+
+    const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'all-tracked-dependencies'), process.pid.toString())
+
+    expect(information).to.deep.eq({
+      envUrl: 'https://example.com',
+      dependencies: { bar: { version: '2.0.0' }, foo: { version: '1.0.0' } },
+    })
+  })
+
+  it('should be able to get the environment for: absent CYPRESS_ENV_URL and all tracked dependencies', async () => {
+    const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'all-tracked-dependencies'), process.pid.toString())
+
+    expect(information).to.deep.eq({
+      dependencies: { bar: { version: '2.0.0' }, foo: { version: '1.0.0' } },
+    })
+  })
+
+  it('should be able to get the environment for: absent CYPRESS_ENV_URL and partial dependencies not matching criteria', async () => {
+    const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'partial-dependencies-not-matching'), process.pid.toString())
+
+    expect(information).to.deep.eq({
+      dependencies: { bar: { version: '2.0.0' } },
+    })
+  })
+
+  context('absent CYPRESS_ENV_URL and partial dependencies matching criteria', () => {
+    it('should be able to get the environment for CYPRESS_ENV_URL defined in grandparent process', async () => {
+      const pid = await spawnProcessTree({
+        grandParentUrl: 'https://grandparent.com',
+      })
+
+      const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'partial-dependencies-matching'), pid.toString())
+
+      expect(information).to.deep.eq({
+        envUrl: 'https://grandparent.com',
+        dependencies: { foo: { version: '1.0.0' } },
+      })
+    })
+
+    it('should be able to get the environment for CYPRESS_ENV_URL defined in parent process', async () => {
+      const pid = await spawnProcessTree({
+        parentUrl: 'https://parent.com',
+      })
+
+      const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'partial-dependencies-matching'), pid.toString())
+
+      expect(information).to.deep.eq({
+        envUrl: 'https://parent.com',
+        dependencies: { foo: { version: '1.0.0' } },
+      })
+    })
+
+    it('should be able to get the environment for CYPRESS_ENV_URL defined in current process', async () => {
+      const pid = await spawnProcessTree({
+        url: 'https://url.com',
+      })
+
+      const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'partial-dependencies-matching'), pid.toString())
+
+      expect(information).to.deep.eq({
+        envUrl: 'https://url.com',
+        dependencies: { foo: { version: '1.0.0' } },
+      })
+    })
+
+    it('should be able to get the environment for CYPRESS_ENV_URL defined in parent process overriding grandparent process', async () => {
+      const pid = await spawnProcessTree({
+        grandParentUrl: 'https://grandparent.com',
+        parentUrl: 'https://parent.com',
+      })
+
+      const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'partial-dependencies-matching'), pid.toString())
+
+      expect(information).to.deep.eq({
+        envUrl: 'https://parent.com',
+        dependencies: { foo: { version: '1.0.0' } },
+      })
+    })
+  })
+})
diff --git a/yarn.lock b/yarn.lock
index 43af4f16c77d..72f6bcec7db9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -25677,6 +25677,13 @@ resolve-options@^1.1.0:
   dependencies:
     value-or-function "^3.0.0"
 
+resolve-package-path@4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/resolve-package-path/-/resolve-package-path-4.0.3.tgz#31dab6897236ea6613c72b83658d88898a9040aa"
+  integrity sha512-SRpNAPW4kewOaNUt8VPqhJ0UMxawMwzJD8V7m1cJfdSTK9ieZwS6K7Dabsm4bmLFM96Z5Y/UznrpG5kt1im8yA==
+  dependencies:
+    path-root "^0.1.1"
+
 resolve-pkg@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/resolve-pkg/-/resolve-pkg-2.0.0.tgz#ac06991418a7623edc119084edc98b0e6bf05a41"

From 5150ac93da021972203ca18dced05fd080f29ae1 Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Wed, 22 Feb 2023 13:41:20 -0600
Subject: [PATCH 25/52] tweak regex

---
 packages/server/lib/cloud/environment.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/server/lib/cloud/environment.ts b/packages/server/lib/cloud/environment.ts
index 5ffa7ed46989..6136cee017b5 100644
--- a/packages/server/lib/cloud/environment.ts
+++ b/packages/server/lib/cloud/environment.ts
@@ -36,7 +36,7 @@ const getCypressEnvUrlFromProcessBranch = async (pid: string) => {
       const { stdout } = await execAsync(`ps eww -p ${processBranch.join(',')} -o pid=,command=`)
 
       const pidEnvUrlMapping = stdout.split('\n').reduce((acc, line) => {
-        const cypressEnvUrl = line.trim().match(/(\d+).*CYPRESS_ENV_URL=(\S+)/)
+        const cypressEnvUrl = line.trim().match(/(\d+)\s.*CYPRESS_ENV_URL=(\S+)\s/)
 
         if (cypressEnvUrl) {
           acc[cypressEnvUrl[1]] = cypressEnvUrl[2]

From 1021b0dcb7243a1135758ee06b641dbbc3831a15 Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Wed, 22 Feb 2023 14:18:20 -0600
Subject: [PATCH 26/52] use map instead of object

---
 packages/server/lib/cloud/environment.ts | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/packages/server/lib/cloud/environment.ts b/packages/server/lib/cloud/environment.ts
index 6136cee017b5..82f2073a8271 100644
--- a/packages/server/lib/cloud/environment.ts
+++ b/packages/server/lib/cloud/environment.ts
@@ -11,16 +11,16 @@ const getProcessBranchForPid = async (pid: string) => {
   const processTree = stdout.split('\n').reduce((acc, line) => {
     const [pid, ppid] = line.trim().split(/\s+/)
 
-    acc[pid] = ppid
+    acc.set(pid, ppid)
 
     return acc
-  }, {})
+  }, new Map())
 
   const currentProcessBranch: string[] = []
 
   while (pid) {
     currentProcessBranch.push(pid)
-    pid = processTree[pid]
+    pid = processTree.get(pid)
   }
 
   return currentProcessBranch
@@ -39,16 +39,16 @@ const getCypressEnvUrlFromProcessBranch = async (pid: string) => {
         const cypressEnvUrl = line.trim().match(/(\d+)\s.*CYPRESS_ENV_URL=(\S+)\s/)
 
         if (cypressEnvUrl) {
-          acc[cypressEnvUrl[1]] = cypressEnvUrl[2]
+          acc.set(cypressEnvUrl[1], cypressEnvUrl[2])
         }
 
         return acc
-      }, {})
+      }, new Map())
 
-      const foundPid = processBranch.find((pid) => pidEnvUrlMapping[pid])
+      const foundPid = processBranch.find((pid) => pidEnvUrlMapping.get(pid))
 
       if (foundPid) {
-        envUrl = pidEnvUrlMapping[foundPid]
+        envUrl = pidEnvUrlMapping.get(foundPid)
       }
     } catch (err) {
       error = err

From 329b6557afda121403d98d687d2e88a676bf18f1 Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Wed, 22 Feb 2023 16:37:12 -0600
Subject: [PATCH 27/52] fix tests

---
 packages/server/lib/cloud/environment.ts      | 62 +++++++++++++------
 .../fixtures/cloud/environment/.gitignore     |  1 +
 .../node_modules/bar/package.json             |  6 ++
 .../node_modules/bar/src/index.js             |  0
 .../node_modules/foo/index.js                 |  0
 .../node_modules/foo/package.json             |  6 ++
 .../node_modules/foo/index.js                 |  0
 .../node_modules/foo/package.json             |  6 ++
 .../node_modules/bar/package.json             |  6 ++
 .../node_modules/bar/src/index.js             |  0
 .../test/unit/cloud/environment_spec.ts       | 10 +++
 .../server/test/unit/modes/record_spec.js     |  1 +
 12 files changed, 80 insertions(+), 18 deletions(-)
 create mode 100644 packages/server/test/support/fixtures/cloud/environment/.gitignore
 create mode 100644 packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/bar/package.json
 create mode 100644 packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/bar/src/index.js
 create mode 100644 packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/foo/index.js
 create mode 100644 packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/foo/package.json
 create mode 100644 packages/server/test/support/fixtures/cloud/environment/partial-dependencies-matching/node_modules/foo/index.js
 create mode 100644 packages/server/test/support/fixtures/cloud/environment/partial-dependencies-matching/node_modules/foo/package.json
 create mode 100644 packages/server/test/support/fixtures/cloud/environment/partial-dependencies-not-matching/node_modules/bar/package.json
 create mode 100644 packages/server/test/support/fixtures/cloud/environment/partial-dependencies-not-matching/node_modules/bar/src/index.js

diff --git a/packages/server/lib/cloud/environment.ts b/packages/server/lib/cloud/environment.ts
index 82f2073a8271..9806810f85ef 100644
--- a/packages/server/lib/cloud/environment.ts
+++ b/packages/server/lib/cloud/environment.ts
@@ -26,9 +26,19 @@ const getProcessBranchForPid = async (pid: string) => {
   return currentProcessBranch
 }
 
-const getCypressEnvUrlFromProcessBranch = async (pid: string) => {
+interface GetCypressEnvUrlFromProcessBranch {
+  envUrl?: string
+  error?: {
+    dependency?: string
+    name: string
+    message: string
+    stack: string
+  }
+}
+
+const getCypressEnvUrlFromProcessBranch = async (pid: string): Promise => {
   let error: { name: string, message: string, stack: string } | undefined
-  let envUrl
+  let envUrl: string | undefined
 
   if (process.platform !== 'win32') {
     try {
@@ -66,21 +76,19 @@ const getEnvInformationForProjectRoot = async (projectRoot: string, pid: string)
   let errors: { dependency?: string, name: string, message: string, stack: string }[] = []
   let envDependenciesVar = process.env.CYPRESS_ENV_DEPENDENCIES
   let envUrl = process.env.CYPRESS_ENV_URL
+  let processTreePromise: Promise = !envUrl ? getCypressEnvUrlFromProcessBranch(pid) : Promise.resolve({})
 
   if (envDependenciesVar) {
-    let checkProcessTree = true
     const envDependenciesInformation = JSON.parse(base64Url.decode(envDependenciesVar)) as Record
 
-    await Promise.all(Object.entries(envDependenciesInformation).map(async ([dependency, { processTreeRequirement }]) => {
+    const packageToJsonMapping: Record = {}
+
+    Object.entries(envDependenciesInformation).map(([dependency, { processTreeRequirement }]) => {
       try {
         const packageJsonPath = resolvePackagePath(dependency, projectRoot)
 
         if (packageJsonPath) {
-          const packageVersion = (await fs.readJSON(packageJsonPath)).version
-
-          dependencies[dependency] = {
-            version: packageVersion,
-          }
+          packageToJsonMapping[dependency] = packageJsonPath
         }
       } catch (error) {
         errors.push({
@@ -90,19 +98,37 @@ const getEnvInformationForProjectRoot = async (projectRoot: string, pid: string)
           stack: error.stack,
         })
       }
-      if (processTreeRequirement === 'presence required' && !dependencies[dependency]) {
-        checkProcessTree = false
-      } else if (processTreeRequirement === 'absence required' && dependencies[dependency]) {
-        checkProcessTree = false
+      if (processTreeRequirement === 'presence required' && !packageToJsonMapping[dependency]) {
+        processTreePromise = Promise.resolve({})
+      } else if (processTreeRequirement === 'absence required' && packageToJsonMapping[dependency]) {
+        processTreePromise = Promise.resolve({})
       }
-    }))
+    })
+
+    const [{ envUrl: processTreeEnvUrl, error: processTreeError }] = await Promise.all([
+      processTreePromise,
+      ...Object.entries(packageToJsonMapping).map(async ([dependency, packageJsonPath]) => {
+        try {
+          const packageVersion = (await fs.readJSON(packageJsonPath)).version
 
-    if (!envUrl && checkProcessTree) {
-      const { envUrl: processTreeEnvUrl, error } = await getCypressEnvUrlFromProcessBranch(pid)
+          dependencies[dependency] = {
+            version: packageVersion,
+          }
+        } catch (error) {
+          errors.push({
+            dependency,
+            name: error.name,
+            message: error.message,
+            stack: error.stack,
+          })
+        }
+      }),
+    ])
 
+    if (processTreeEnvUrl || processTreeError) {
       envUrl = processTreeEnvUrl
-      if (error) {
-        errors.push(error)
+      if (processTreeError) {
+        errors.push(processTreeError)
       }
     }
   }
diff --git a/packages/server/test/support/fixtures/cloud/environment/.gitignore b/packages/server/test/support/fixtures/cloud/environment/.gitignore
new file mode 100644
index 000000000000..736e8ae58ad8
--- /dev/null
+++ b/packages/server/test/support/fixtures/cloud/environment/.gitignore
@@ -0,0 +1 @@
+!node_modules
\ No newline at end of file
diff --git a/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/bar/package.json b/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/bar/package.json
new file mode 100644
index 000000000000..55f760d8d406
--- /dev/null
+++ b/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/bar/package.json
@@ -0,0 +1,6 @@
+{
+  "name": "bar",
+  "version": "2.0.0",
+  "main": "src/index.js",
+  "type": "module"
+}
diff --git a/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/bar/src/index.js b/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/bar/src/index.js
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/foo/index.js b/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/foo/index.js
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/foo/package.json b/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/foo/package.json
new file mode 100644
index 000000000000..44290b9c0cbd
--- /dev/null
+++ b/packages/server/test/support/fixtures/cloud/environment/all-tracked-dependencies/node_modules/foo/package.json
@@ -0,0 +1,6 @@
+{
+  "name": "foo",
+  "version": "1.0.0",
+  "main": "index.js",
+  "type": "module"
+}
diff --git a/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-matching/node_modules/foo/index.js b/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-matching/node_modules/foo/index.js
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-matching/node_modules/foo/package.json b/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-matching/node_modules/foo/package.json
new file mode 100644
index 000000000000..44290b9c0cbd
--- /dev/null
+++ b/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-matching/node_modules/foo/package.json
@@ -0,0 +1,6 @@
+{
+  "name": "foo",
+  "version": "1.0.0",
+  "main": "index.js",
+  "type": "module"
+}
diff --git a/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-not-matching/node_modules/bar/package.json b/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-not-matching/node_modules/bar/package.json
new file mode 100644
index 000000000000..55f760d8d406
--- /dev/null
+++ b/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-not-matching/node_modules/bar/package.json
@@ -0,0 +1,6 @@
+{
+  "name": "bar",
+  "version": "2.0.0",
+  "main": "src/index.js",
+  "type": "module"
+}
diff --git a/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-not-matching/node_modules/bar/src/index.js b/packages/server/test/support/fixtures/cloud/environment/partial-dependencies-not-matching/node_modules/bar/src/index.js
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/packages/server/test/unit/cloud/environment_spec.ts b/packages/server/test/unit/cloud/environment_spec.ts
index 0d61f11a0bfc..5632882071b0 100644
--- a/packages/server/test/unit/cloud/environment_spec.ts
+++ b/packages/server/test/unit/cloud/environment_spec.ts
@@ -134,5 +134,15 @@ describe('lib/cloud/api', () => {
         dependencies: { foo: { version: '1.0.0' } },
       })
     })
+
+    it('should return no envUrl when CYPRESS_ENV_URL is not defined in any parent process', async () => {
+      const pid = await spawnProcessTree({})
+
+      const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'partial-dependencies-matching'), pid.toString())
+
+      expect(information).to.deep.eq({
+        dependencies: { foo: { version: '1.0.0' } },
+      })
+    })
   })
 })
diff --git a/packages/server/test/unit/modes/record_spec.js b/packages/server/test/unit/modes/record_spec.js
index 55bb06115ec8..8c13bd0c1eae 100644
--- a/packages/server/test/unit/modes/record_spec.js
+++ b/packages/server/test/unit/modes/record_spec.js
@@ -306,6 +306,7 @@ describe('lib/modes/record', () => {
           expect(commitInfo.commitInfo).to.be.calledWith(projectRoot)
 
           expect(api.createRun).to.be.calledWith({
+            projectRoot,
             group,
             parallel,
             projectId,

From fcbada817ba9f5c5acbd87a2f2c36d116cc3af87 Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Wed, 22 Feb 2023 16:41:08 -0600
Subject: [PATCH 28/52] fix tests

---
 packages/server/lib/cloud/api.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts
index 41d4e4196146..671272c25759 100644
--- a/packages/server/lib/cloud/api.ts
+++ b/packages/server/lib/cloud/api.ts
@@ -480,7 +480,7 @@ module.exports = {
           url: `${baseUrl}preflight`,
           body: {
             apiUrl,
-            ...await getEnvInformationForProjectRoot(projectRoot),
+            ...await getEnvInformationForProjectRoot(projectRoot, process.pid.toString()),
             ...preflightInfo,
           },
           headers: {

From 7e026ae95c75e62a6aed0452557f003ffe7bf0bb Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Wed, 22 Feb 2023 17:04:44 -0600
Subject: [PATCH 29/52] fix binary build

---
 scripts/after-pack-hook.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/scripts/after-pack-hook.js b/scripts/after-pack-hook.js
index 2e0a6d461642..485c9bb47dd1 100644
--- a/scripts/after-pack-hook.js
+++ b/scripts/after-pack-hook.js
@@ -60,7 +60,7 @@ module.exports = async function (params) {
 
     if (!['1', 'true'].includes(process.env.DISABLE_SNAPSHOT_REQUIRE)) {
       const binaryEntryPointSource = await getBinaryEntryPointSource()
-      const encryptionFilePath = path.join(CY_ROOT_DIR, 'packages/server/lib/cloud/encryption.ts')
+      const encryptionFilePath = path.join(CY_ROOT_DIR, 'packages/server/lib/cloud/environment.ts')
       const encryptionFileSource = await getEncryptionFileSource(encryptionFilePath)
       const cloudApiFilePath = path.join(CY_ROOT_DIR, 'packages/server/lib/cloud/api.ts')
       const cloudApiFileSource = await getCloudApiFileSource(cloudApiFilePath)

From fed61c193043ea453c555d9ef6e03c1a86cacdb1 Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Wed, 22 Feb 2023 17:32:53 -0600
Subject: [PATCH 30/52] oops wrong name

---
 packages/server/lib/cloud/environment.ts      |  4 +--
 .../cloud/environment/test-project/child.js   |  4 +--
 .../environment/test-project/grandchild.js    |  2 +-
 .../cloud/environment/test-project/index.js   |  4 +--
 packages/server/test/unit/cloud/api_spec.js   |  1 -
 .../test/unit/cloud/environment_spec.ts       | 28 +++++++++----------
 scripts/binary/binary-sources.js              | 14 +++++-----
 7 files changed, 28 insertions(+), 29 deletions(-)

diff --git a/packages/server/lib/cloud/environment.ts b/packages/server/lib/cloud/environment.ts
index 9806810f85ef..302b830f0e86 100644
--- a/packages/server/lib/cloud/environment.ts
+++ b/packages/server/lib/cloud/environment.ts
@@ -46,7 +46,7 @@ const getCypressEnvUrlFromProcessBranch = async (pid: string): Promise {
-        const cypressEnvUrl = line.trim().match(/(\d+)\s.*CYPRESS_ENV_URL=(\S+)\s/)
+        const cypressEnvUrl = line.trim().match(/(\d+)\s.*CYPRESS_API_URL=(\S+)\s/)
 
         if (cypressEnvUrl) {
           acc.set(cypressEnvUrl[1], cypressEnvUrl[2])
@@ -75,7 +75,7 @@ const getEnvInformationForProjectRoot = async (projectRoot: string, pid: string)
   let dependencies = {}
   let errors: { dependency?: string, name: string, message: string, stack: string }[] = []
   let envDependenciesVar = process.env.CYPRESS_ENV_DEPENDENCIES
-  let envUrl = process.env.CYPRESS_ENV_URL
+  let envUrl = process.env.CYPRESS_API_URL
   let processTreePromise: Promise = !envUrl ? getCypressEnvUrlFromProcessBranch(pid) : Promise.resolve({})
 
   if (envDependenciesVar) {
diff --git a/packages/server/test/support/fixtures/cloud/environment/test-project/child.js b/packages/server/test/support/fixtures/cloud/environment/test-project/child.js
index 901860d9f630..ae77d3f731e0 100644
--- a/packages/server/test/support/fixtures/cloud/environment/test-project/child.js
+++ b/packages/server/test/support/fixtures/cloud/environment/test-project/child.js
@@ -3,7 +3,7 @@ import path from 'path'
 import * as url from 'url'
 
 // eslint-disable-next-line no-console
-console.log('child', process.pid, process.ppid, process.env.CHILD_CYPRESS_ENV_URL)
+console.log('child', process.pid, process.ppid, process.env.CHILD_CYPRESS_API_URL)
 
 const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
 
@@ -12,7 +12,7 @@ const proc = spawn('node', ['grandchild.js'], {
   stdio: 'inherit',
   env: {
     ...process.env,
-    CYPRESS_ENV_URL: process.env.PARENT_CYPRESS_ENV_URL,
+    CYPRESS_API_URL: process.env.PARENT_CYPRESS_API_URL,
   },
 })
 
diff --git a/packages/server/test/support/fixtures/cloud/environment/test-project/grandchild.js b/packages/server/test/support/fixtures/cloud/environment/test-project/grandchild.js
index 6b3a2b15dea2..8a96cb8fbc16 100644
--- a/packages/server/test/support/fixtures/cloud/environment/test-project/grandchild.js
+++ b/packages/server/test/support/fixtures/cloud/environment/test-project/grandchild.js
@@ -1,5 +1,5 @@
 // eslint-disable-next-line no-console
-console.log('grandchild', process.pid, process.ppid, process.env.GRANDCHILD_CYPRESS_ENV_URL)
+console.log('grandchild', process.pid, process.ppid, process.env.GRANDCHILD_CYPRESS_API_URL)
 
 const timeout = setTimeout(() => {
 
diff --git a/packages/server/test/support/fixtures/cloud/environment/test-project/index.js b/packages/server/test/support/fixtures/cloud/environment/test-project/index.js
index 218f9d248b1c..abba1c052854 100644
--- a/packages/server/test/support/fixtures/cloud/environment/test-project/index.js
+++ b/packages/server/test/support/fixtures/cloud/environment/test-project/index.js
@@ -3,7 +3,7 @@ import path from 'path'
 import * as url from 'url'
 
 // eslint-disable-next-line no-console
-console.log('parent', process.pid, process.ppid, process.env.PARENT_CYPRESS_ENV_URL)
+console.log('parent', process.pid, process.ppid, process.env.PARENT_CYPRESS_API_URL)
 
 const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
 
@@ -12,7 +12,7 @@ const proc = spawn('node', ['child.js'], {
   stdio: 'inherit',
   env: {
     ...process.env,
-    CYPRESS_ENV_URL: process.env.PARENT_CYPRESS_ENV_URL,
+    CYPRESS_API_URL: process.env.PARENT_CYPRESS_API_URL,
   },
 })
 
diff --git a/packages/server/test/unit/cloud/api_spec.js b/packages/server/test/unit/cloud/api_spec.js
index 499551a85f60..9c69c35527fe 100644
--- a/packages/server/test/unit/cloud/api_spec.js
+++ b/packages/server/test/unit/cloud/api_spec.js
@@ -228,7 +228,6 @@ describe('lib/cloud/api', () => {
       sinon.restore()
       sinon.stub(os, 'platform').returns('linux')
 
-      process.env.CYPRESS_ENV_URL = 'https://some.server.com'
       process.env.CYPRESS_CONFIG_ENV = 'production'
       process.env.CYPRESS_API_URL = 'https://some.server.com'
 
diff --git a/packages/server/test/unit/cloud/environment_spec.ts b/packages/server/test/unit/cloud/environment_spec.ts
index 5632882071b0..a331b3a721b3 100644
--- a/packages/server/test/unit/cloud/environment_spec.ts
+++ b/packages/server/test/unit/cloud/environment_spec.ts
@@ -6,7 +6,7 @@ import { exec } from 'child_process'
 
 describe('lib/cloud/api', () => {
   beforeEach(() => {
-    delete process.env.CYPRESS_ENV_URL
+    delete process.env.CYPRESS_API_URL
     process.env.CYPRESS_ENV_DEPENDENCIES = base64url.encode(JSON.stringify({
       'foo': {
         processTreeRequirement: 'presence required',
@@ -32,9 +32,9 @@ describe('lib/cloud/api', () => {
         cwd: path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'test-project'),
         env: {
           ...process.env,
-          CYPRESS_ENV_URL: grandParentUrl,
-          CHILD_CYPRESS_ENV_URL: parentUrl,
-          GRANDCHILD_CYPRESS_ENV_URL: url,
+          CYPRESS_API_URL: grandParentUrl,
+          CHILD_CYPRESS_API_URL: parentUrl,
+          GRANDCHILD_CYPRESS_API_URL: url,
         },
       })
 
@@ -54,8 +54,8 @@ describe('lib/cloud/api', () => {
     }
   })
 
-  it('should be able to get the environment for: present CYPRESS_ENV_URL and all tracked dependencies', async () => {
-    process.env.CYPRESS_ENV_URL = 'https://example.com'
+  it('should be able to get the environment for: present CYPRESS_API_URL and all tracked dependencies', async () => {
+    process.env.CYPRESS_API_URL = 'https://example.com'
 
     const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'all-tracked-dependencies'), process.pid.toString())
 
@@ -65,7 +65,7 @@ describe('lib/cloud/api', () => {
     })
   })
 
-  it('should be able to get the environment for: absent CYPRESS_ENV_URL and all tracked dependencies', async () => {
+  it('should be able to get the environment for: absent CYPRESS_API_URL and all tracked dependencies', async () => {
     const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'all-tracked-dependencies'), process.pid.toString())
 
     expect(information).to.deep.eq({
@@ -73,7 +73,7 @@ describe('lib/cloud/api', () => {
     })
   })
 
-  it('should be able to get the environment for: absent CYPRESS_ENV_URL and partial dependencies not matching criteria', async () => {
+  it('should be able to get the environment for: absent CYPRESS_API_URL and partial dependencies not matching criteria', async () => {
     const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'partial-dependencies-not-matching'), process.pid.toString())
 
     expect(information).to.deep.eq({
@@ -81,8 +81,8 @@ describe('lib/cloud/api', () => {
     })
   })
 
-  context('absent CYPRESS_ENV_URL and partial dependencies matching criteria', () => {
-    it('should be able to get the environment for CYPRESS_ENV_URL defined in grandparent process', async () => {
+  context('absent CYPRESS_API_URL and partial dependencies matching criteria', () => {
+    it('should be able to get the environment for CYPRESS_API_URL defined in grandparent process', async () => {
       const pid = await spawnProcessTree({
         grandParentUrl: 'https://grandparent.com',
       })
@@ -95,7 +95,7 @@ describe('lib/cloud/api', () => {
       })
     })
 
-    it('should be able to get the environment for CYPRESS_ENV_URL defined in parent process', async () => {
+    it('should be able to get the environment for CYPRESS_API_URL defined in parent process', async () => {
       const pid = await spawnProcessTree({
         parentUrl: 'https://parent.com',
       })
@@ -108,7 +108,7 @@ describe('lib/cloud/api', () => {
       })
     })
 
-    it('should be able to get the environment for CYPRESS_ENV_URL defined in current process', async () => {
+    it('should be able to get the environment for CYPRESS_API_URL defined in current process', async () => {
       const pid = await spawnProcessTree({
         url: 'https://url.com',
       })
@@ -121,7 +121,7 @@ describe('lib/cloud/api', () => {
       })
     })
 
-    it('should be able to get the environment for CYPRESS_ENV_URL defined in parent process overriding grandparent process', async () => {
+    it('should be able to get the environment for CYPRESS_API_URL defined in parent process overriding grandparent process', async () => {
       const pid = await spawnProcessTree({
         grandParentUrl: 'https://grandparent.com',
         parentUrl: 'https://parent.com',
@@ -135,7 +135,7 @@ describe('lib/cloud/api', () => {
       })
     })
 
-    it('should return no envUrl when CYPRESS_ENV_URL is not defined in any parent process', async () => {
+    it('should return no envUrl when CYPRESS_API_URL is not defined in any parent process', async () => {
       const pid = await spawnProcessTree({})
 
       const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'partial-dependencies-matching'), pid.toString())
diff --git a/scripts/binary/binary-sources.js b/scripts/binary/binary-sources.js
index ef0560b9f9bc..dcf3ff1fd302 100644
--- a/scripts/binary/binary-sources.js
+++ b/scripts/binary/binary-sources.js
@@ -61,23 +61,23 @@ const validateEncryptionFile = async (encryptionFilePath) => {
 const getCloudApiFileSource = async (cloudApiFilePath) => {
   const fileContents = await fs.readFile(cloudApiFilePath, 'utf8')
 
-  if (!fileContents.includes('process.env.CYPRESS_ENV_URLS')) {
-    throw new Error(`Expected to find CYPRESS_ENV_URLS in cloud api file`)
+  if (!fileContents.includes('process.env.CYPRESS_ENV_DEPENDENCIES')) {
+    throw new Error(`Expected to find CYPRESS_ENV_DEPENDENCIES in cloud api file`)
   }
 
-  if (process.env.CYPRESS_ENV_URLS) {
-    return fileContents.replace('process.env.CYPRESS_ENV_URLS', `'${process.env.CYPRESS_ENV_URLS}'`)
+  if (process.env.CYPRESS_ENV_DEPENDENCIES) {
+    return fileContents.replace('process.env.CYPRESS_ENV_DEPENDENCIES', `'${process.env.CYPRESS_ENV_DEPENDENCIES}'`)
   }
 
   return fileContents
 }
 
 const validateCloudApiFile = async (cloudApiFilePath) => {
-  if (process.env.CYPRESS_ENV_URLS) {
+  if (process.env.CYPRESS_ENV_DEPENDENCIES) {
     const afterReplaceCloudApi = await fs.readFile(cloudApiFilePath, 'utf8')
 
-    if (afterReplaceCloudApi.includes('process.env.CYPRESS_ENV_URLS')) {
-      throw new Error(`Expected process.env.CYPRESS_ENV_URLS to be stripped from cloud api file`)
+    if (afterReplaceCloudApi.includes('process.env.CYPRESS_ENV_DEPENDENCIES')) {
+      throw new Error(`Expected process.env.CYPRESS_ENV_DEPENDENCIES to be stripped from cloud api file`)
     }
   }
 }

From e9e3547cead876e91a4d61ff5ee7fa2e3edd2073 Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Wed, 22 Feb 2023 18:23:22 -0600
Subject: [PATCH 31/52] run environment tests on all platforms

---
 .circleci/workflows.yml | 31 ++++++++++++++++++++++++++++++-
 1 file changed, 30 insertions(+), 1 deletion(-)

diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml
index 686e662911a3..294714e78e33 100644
--- a/.circleci/workflows.yml
+++ b/.circleci/workflows.yml
@@ -30,6 +30,7 @@ mainBuildFilters: &mainBuildFilters
         - /^release\/\d+\.\d+\.\d+$/
         # use the following branch as well to ensure that v8 snapshot cache updates are fully tested
         - 'update-v8-snapshot-cache-on-develop'
+        - 'fix/preflight'
 
 # usually we don't build Mac app - it takes a long time
 # but sometimes we want to really confirm we are doing the right thing
@@ -40,6 +41,7 @@ macWorkflowFilters: &darwin-workflow-filters
     - equal: [ develop, << pipeline.git.branch >> ]
     # use the following branch as well to ensure that v8 snapshot cache updates are fully tested
     - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
+    - equal: [ 'fix/preflight', << pipeline.git.branch >> ]
     - matches:
         pattern: /^release\/\d+\.\d+\.\d+$/
         value: << pipeline.git.branch >>
@@ -50,6 +52,7 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters
     - equal: [ develop, << pipeline.git.branch >> ]
     # use the following branch as well to ensure that v8 snapshot cache updates are fully tested
     - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
+    - equal: [ 'fix/preflight', << pipeline.git.branch >> ]
     - matches:
         pattern: /^release\/\d+\.\d+\.\d+$/
         value: << pipeline.git.branch >>
@@ -69,6 +72,7 @@ windowsWorkflowFilters: &windows-workflow-filters
     - equal: [ develop, << pipeline.git.branch >> ]
     # use the following branch as well to ensure that v8 snapshot cache updates are fully tested
     - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
+    - equal: [ 'fix/preflight', << pipeline.git.branch >> ]
     - matches:
         pattern: /^release\/\d+\.\d+\.\d+$/
         value: << pipeline.git.branch >>
@@ -134,7 +138,7 @@ commands:
       - run:
           name: Check current branch to persist artifacts
           command: |
-            if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* ]]; then
+            if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "fix/preflight" && "$CIRCLE_BRANCH" != "update-v8-snapshot-cache-on-develop" ]]; then
               echo "Not uploading artifacts or posting install comment for this branch."
               circleci-agent step halt
             fi
@@ -1483,6 +1487,18 @@ jobs:
           path: /tmp/cypress
       - store-npm-logs
 
+  server-unit-tests-cloud-environment:
+    <<: *defaults
+    parallelism: 1
+    steps:
+      - restore_cached_workspace
+      - run: yarn workspace @packages/server test-unit cloud/environment_spec.ts
+      - verify-mocha-results:
+          expectedResultCount: 1
+      - store_test_results:
+          path: /tmp/cypress
+      - store-npm-logs
+
   server-integration-tests:
     <<: *defaults
     parallelism: 1
@@ -2738,6 +2754,12 @@ darwin-x64-workflow: &darwin-x64-workflow
         resource_class: macos.x86.medium.gen2
         requires:
           - darwin-x64-build
+    - server-unit-tests-cloud-environment:
+        name: darwin-x64-driver-server-unit-tests-cloud-environment
+        executor: mac
+        resource_class: macos.x86.medium.gen2
+        requires:
+          - darwin-x64-build
 
 darwin-arm64-workflow: &darwin-arm64-workflow
   jobs:
@@ -2817,6 +2839,13 @@ windows-workflow: &windows-workflow
         requires:
           - windows-build
 
+    - server-unit-tests-cloud-environment:
+        name: windows-server-unit-tests-cloud-environment
+        executor: windows
+        resource_class: windows.large
+        requires:
+          - windows-build
+
     - create-build-artifacts:
         name: windows-create-build-artifacts
         executor: windows

From 57a821d1cabfa78c7804335ded683b9ffee9eaea Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Wed, 22 Feb 2023 18:31:52 -0600
Subject: [PATCH 32/52] fix build

---
 .circleci/workflows.yml | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml
index 294714e78e33..b03e7b2742a4 100644
--- a/.circleci/workflows.yml
+++ b/.circleci/workflows.yml
@@ -1489,6 +1489,12 @@ jobs:
 
   server-unit-tests-cloud-environment:
     <<: *defaults
+    parameters:
+      <<: *defaultsParameters
+      resource_class:
+        type: string
+        default: medium
+    resource_class: << parameters.resource_class >>
     parallelism: 1
     steps:
       - restore_cached_workspace

From 809d5b1035663ea85922bb1a286a24dfc3e9e787 Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Wed, 22 Feb 2023 19:35:15 -0600
Subject: [PATCH 33/52] fix tests and build

---
 packages/server/lib/cloud/environment.ts            | 2 +-
 packages/server/test/unit/cloud/environment_spec.ts | 8 ++++----
 scripts/after-pack-hook.js                          | 4 ++--
 3 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/packages/server/lib/cloud/environment.ts b/packages/server/lib/cloud/environment.ts
index 302b830f0e86..35a071acec68 100644
--- a/packages/server/lib/cloud/environment.ts
+++ b/packages/server/lib/cloud/environment.ts
@@ -18,7 +18,7 @@ const getProcessBranchForPid = async (pid: string) => {
 
   const currentProcessBranch: string[] = []
 
-  while (pid) {
+  while (pid && pid !== '0') {
     currentProcessBranch.push(pid)
     pid = processTree.get(pid)
   }
diff --git a/packages/server/test/unit/cloud/environment_spec.ts b/packages/server/test/unit/cloud/environment_spec.ts
index a331b3a721b3..41297a152ef3 100644
--- a/packages/server/test/unit/cloud/environment_spec.ts
+++ b/packages/server/test/unit/cloud/environment_spec.ts
@@ -90,7 +90,7 @@ describe('lib/cloud/api', () => {
       const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'partial-dependencies-matching'), pid.toString())
 
       expect(information).to.deep.eq({
-        envUrl: 'https://grandparent.com',
+        ...(process.platform === 'win32' ? { envUrl: 'https://grandparent.com' } : {}),
         dependencies: { foo: { version: '1.0.0' } },
       })
     })
@@ -103,7 +103,7 @@ describe('lib/cloud/api', () => {
       const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'partial-dependencies-matching'), pid.toString())
 
       expect(information).to.deep.eq({
-        envUrl: 'https://parent.com',
+        ...(process.platform === 'win32' ? { envUrl: 'https://parent.com' } : {}),
         dependencies: { foo: { version: '1.0.0' } },
       })
     })
@@ -116,7 +116,7 @@ describe('lib/cloud/api', () => {
       const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'partial-dependencies-matching'), pid.toString())
 
       expect(information).to.deep.eq({
-        envUrl: 'https://url.com',
+        ...(process.platform === 'win32' ? { envUrl: 'https://url.com' } : {}),
         dependencies: { foo: { version: '1.0.0' } },
       })
     })
@@ -130,7 +130,7 @@ describe('lib/cloud/api', () => {
       const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'partial-dependencies-matching'), pid.toString())
 
       expect(information).to.deep.eq({
-        envUrl: 'https://parent.com',
+        ...(process.platform === 'win32' ? { envUrl: 'https://parent.com' } : {}),
         dependencies: { foo: { version: '1.0.0' } },
       })
     })
diff --git a/scripts/after-pack-hook.js b/scripts/after-pack-hook.js
index 485c9bb47dd1..fa28fe6d289e 100644
--- a/scripts/after-pack-hook.js
+++ b/scripts/after-pack-hook.js
@@ -60,9 +60,9 @@ module.exports = async function (params) {
 
     if (!['1', 'true'].includes(process.env.DISABLE_SNAPSHOT_REQUIRE)) {
       const binaryEntryPointSource = await getBinaryEntryPointSource()
-      const encryptionFilePath = path.join(CY_ROOT_DIR, 'packages/server/lib/cloud/environment.ts')
+      const encryptionFilePath = path.join(CY_ROOT_DIR, 'packages/server/lib/cloud/encryption.ts')
       const encryptionFileSource = await getEncryptionFileSource(encryptionFilePath)
-      const cloudApiFilePath = path.join(CY_ROOT_DIR, 'packages/server/lib/cloud/api.ts')
+      const cloudApiFilePath = path.join(CY_ROOT_DIR, 'packages/server/lib/cloud/environment.ts')
       const cloudApiFileSource = await getCloudApiFileSource(cloudApiFilePath)
 
       await Promise.all([

From a1de33080d6ad6a81ffcbe2ff05e7e4086a0100a Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Wed, 22 Feb 2023 20:21:09 -0600
Subject: [PATCH 34/52] pr comments

---
 .circleci/workflows.yml          |  4 ++--
 packages/server/lib/cloud/api.ts | 11 +++++++----
 2 files changed, 9 insertions(+), 6 deletions(-)

diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml
index b03e7b2742a4..eb83129ba779 100644
--- a/.circleci/workflows.yml
+++ b/.circleci/workflows.yml
@@ -2763,7 +2763,7 @@ darwin-x64-workflow: &darwin-x64-workflow
     - server-unit-tests-cloud-environment:
         name: darwin-x64-driver-server-unit-tests-cloud-environment
         executor: mac
-        resource_class: macos.x86.medium.gen2
+        resource_class: macos.x86.small.gen2
         requires:
           - darwin-x64-build
 
@@ -2848,7 +2848,7 @@ windows-workflow: &windows-workflow
     - server-unit-tests-cloud-environment:
         name: windows-server-unit-tests-cloud-environment
         executor: windows
-        resource_class: windows.large
+        resource_class: windows.small
         requires:
           - windows-build
 
diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts
index 671272c25759..cfb33a038ecd 100644
--- a/packages/server/lib/cloud/api.ts
+++ b/packages/server/lib/cloud/api.ts
@@ -475,12 +475,15 @@ module.exports = {
 
       const preflightBaseProxy = apiUrl.replace('api', 'api-proxy')
 
-      const makeReq = async (baseUrl, agent) => {
+      const envInformation = await getEnvInformationForProjectRoot(projectRoot, process.pid.toString())
+      const makeReq = async ({ baseUrl, agent }) => {
         return rp.post({
           url: `${baseUrl}preflight`,
           body: {
             apiUrl,
-            ...await getEnvInformationForProjectRoot(projectRoot, process.pid.toString()),
+            envUrl: envInformation.envUrl,
+            dependencies: envInformation.dependencies,
+            errors: envInformation.errors,
             ...preflightInfo,
           },
           headers: {
@@ -495,9 +498,9 @@ module.exports = {
       }
 
       const postReqs = async () => {
-        return makeReq(preflightBaseProxy, null)
+        return makeReq({ baseUrl: preflightBaseProxy, agent: null })
         .catch((err) => {
-          return makeReq(apiUrl, agent)
+          return makeReq({ baseUrl: apiUrl, agent })
         })
       }
 

From e358a663e29e67a10a15cbd17f191d2f472d57b0 Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Wed, 22 Feb 2023 20:26:12 -0600
Subject: [PATCH 35/52] renaming

---
 packages/server/lib/cloud/environment.ts            | 8 ++++----
 packages/server/test/unit/cloud/environment_spec.ts | 4 ++--
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/packages/server/lib/cloud/environment.ts b/packages/server/lib/cloud/environment.ts
index 35a071acec68..5a31264ec435 100644
--- a/packages/server/lib/cloud/environment.ts
+++ b/packages/server/lib/cloud/environment.ts
@@ -79,11 +79,11 @@ const getEnvInformationForProjectRoot = async (projectRoot: string, pid: string)
   let processTreePromise: Promise = !envUrl ? getCypressEnvUrlFromProcessBranch(pid) : Promise.resolve({})
 
   if (envDependenciesVar) {
-    const envDependenciesInformation = JSON.parse(base64Url.decode(envDependenciesVar)) as Record
+    const envDependenciesInformation = JSON.parse(base64Url.decode(envDependenciesVar)) as Record
 
     const packageToJsonMapping: Record = {}
 
-    Object.entries(envDependenciesInformation).map(([dependency, { processTreeRequirement }]) => {
+    Object.entries(envDependenciesInformation).map(([dependency, { processTreeCheckRequirement }]) => {
       try {
         const packageJsonPath = resolvePackagePath(dependency, projectRoot)
 
@@ -98,9 +98,9 @@ const getEnvInformationForProjectRoot = async (projectRoot: string, pid: string)
           stack: error.stack,
         })
       }
-      if (processTreeRequirement === 'presence required' && !packageToJsonMapping[dependency]) {
+      if (processTreeCheckRequirement === 'presence required' && !packageToJsonMapping[dependency]) {
         processTreePromise = Promise.resolve({})
-      } else if (processTreeRequirement === 'absence required' && packageToJsonMapping[dependency]) {
+      } else if (processTreeCheckRequirement === 'absence required' && packageToJsonMapping[dependency]) {
         processTreePromise = Promise.resolve({})
       }
     })
diff --git a/packages/server/test/unit/cloud/environment_spec.ts b/packages/server/test/unit/cloud/environment_spec.ts
index 41297a152ef3..0d5d608bf2a2 100644
--- a/packages/server/test/unit/cloud/environment_spec.ts
+++ b/packages/server/test/unit/cloud/environment_spec.ts
@@ -9,10 +9,10 @@ describe('lib/cloud/api', () => {
     delete process.env.CYPRESS_API_URL
     process.env.CYPRESS_ENV_DEPENDENCIES = base64url.encode(JSON.stringify({
       'foo': {
-        processTreeRequirement: 'presence required',
+        processTreeCheckRequirement: 'presence required',
       },
       'bar': {
-        processTreeRequirement: 'absence required',
+        processTreeCheckRequirement: 'absence required',
       },
     }))
   })

From 27402455db69108e72053be04770aec63d92af86 Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Thu, 23 Feb 2023 08:22:59 -0600
Subject: [PATCH 36/52] update resource class to medium

---
 .circleci/workflows.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml
index d60416e07418..e48fc5a987be 100644
--- a/.circleci/workflows.yml
+++ b/.circleci/workflows.yml
@@ -2764,7 +2764,7 @@ darwin-x64-workflow: &darwin-x64-workflow
     - server-unit-tests-cloud-environment:
         name: darwin-x64-driver-server-unit-tests-cloud-environment
         executor: mac
-        resource_class: macos.x86.small.gen2
+        resource_class: macos.x86.medium.gen2
         requires:
           - darwin-x64-build
 
@@ -2849,7 +2849,7 @@ windows-workflow: &windows-workflow
     - server-unit-tests-cloud-environment:
         name: windows-server-unit-tests-cloud-environment
         executor: windows
-        resource_class: windows.small
+        resource_class: windows.medium
         requires:
           - windows-build
 

From 04e4b98c714583592b20eeb8a69015098c7f4626 Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Thu, 23 Feb 2023 10:42:49 -0600
Subject: [PATCH 37/52] pr comments

---
 packages/server/lib/cloud/environment.ts      |  6 +--
 packages/server/test/unit/cloud/api_spec.js   |  6 +++
 .../test/unit/cloud/environment_spec.ts       | 46 +++++++++++++++++--
 3 files changed, 51 insertions(+), 7 deletions(-)

diff --git a/packages/server/lib/cloud/environment.ts b/packages/server/lib/cloud/environment.ts
index 5a31264ec435..a9b46bfcb0d5 100644
--- a/packages/server/lib/cloud/environment.ts
+++ b/packages/server/lib/cloud/environment.ts
@@ -134,9 +134,9 @@ const getEnvInformationForProjectRoot = async (projectRoot: string, pid: string)
   }
 
   return {
-    ...(envUrl ? { envUrl } : {}),
-    ...(errors.length > 0 ? { errors } : {}),
-    ...(Object.keys(dependencies).length > 0 ? { dependencies } : {}),
+    envUrl,
+    errors,
+    dependencies,
   }
 }
 
diff --git a/packages/server/test/unit/cloud/api_spec.js b/packages/server/test/unit/cloud/api_spec.js
index 9c69c35527fe..520993f3fd7e 100644
--- a/packages/server/test/unit/cloud/api_spec.js
+++ b/packages/server/test/unit/cloud/api_spec.js
@@ -245,6 +245,8 @@ describe('lib/cloud/api', () => {
       .reply(200, decryptReqBodyAndRespond({
         reqBody: {
           envUrl: 'https://some.server.com',
+          dependencies: {},
+          errors: [],
           apiUrl: 'https://api.cypress.io/',
           projectId: 'abc123',
         },
@@ -268,6 +270,8 @@ describe('lib/cloud/api', () => {
       .reply(200, decryptReqBodyAndRespond({
         reqBody: {
           envUrl: 'https://some.server.com',
+          dependencies: {},
+          errors: [],
           apiUrl: 'https://api.cypress.io/',
           projectId: 'abc123',
         },
@@ -293,6 +297,8 @@ describe('lib/cloud/api', () => {
       .reply(200, decryptReqBodyAndRespond({
         reqBody: {
           envUrl: 'https://some.server.com',
+          dependencies: {},
+          errors: [],
           apiUrl: 'https://api.cypress.io/',
           projectId: 'abc123',
         },
diff --git a/packages/server/test/unit/cloud/environment_spec.ts b/packages/server/test/unit/cloud/environment_spec.ts
index 0d5d608bf2a2..a8de3b4f5c99 100644
--- a/packages/server/test/unit/cloud/environment_spec.ts
+++ b/packages/server/test/unit/cloud/environment_spec.ts
@@ -3,6 +3,8 @@ import getEnvInformationForProjectRoot from '../../../lib/cloud/environment'
 import path from 'path'
 import base64url from 'base64url'
 import { exec } from 'child_process'
+import originalResolvePackagePath from 'resolve-package-path'
+import proxyquire from 'proxyquire'
 
 describe('lib/cloud/api', () => {
   beforeEach(() => {
@@ -62,14 +64,42 @@ describe('lib/cloud/api', () => {
     expect(information).to.deep.eq({
       envUrl: 'https://example.com',
       dependencies: { bar: { version: '2.0.0' }, foo: { version: '1.0.0' } },
+      errors: [],
     })
   })
 
+  it('should be able to get the environment for: present CYPRESS_API_URL and a thrown error when tracking dependencies', async () => {
+    process.env.CYPRESS_API_URL = 'https://example.com'
+
+    const resolvePackagePath = sinon.stub()
+
+    resolvePackagePath.withArgs('foo', sinon.match.any).throws(new Error('some error'))
+    resolvePackagePath.withArgs('bar', sinon.match.any).callsFake(originalResolvePackagePath)
+    const { default: getEnvInfo } = proxyquire('../../../lib/cloud/environment', {
+      'resolve-package-path': resolvePackagePath,
+    })
+
+    const { errors, ...information } = await getEnvInfo(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'all-tracked-dependencies'), process.pid.toString())
+
+    expect(information).to.deep.eq({
+      envUrl: 'https://example.com',
+      dependencies: { bar: { version: '2.0.0' } },
+    })
+
+    expect(errors).to.have.length(1)
+    expect(errors[0].dependency).to.equal('foo')
+    expect(errors[0].message).to.equal('some error')
+    expect(errors[0].name).to.equal('Error')
+    expect(errors[0].stack).to.include('Error: some error')
+  })
+
   it('should be able to get the environment for: absent CYPRESS_API_URL and all tracked dependencies', async () => {
     const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'all-tracked-dependencies'), process.pid.toString())
 
     expect(information).to.deep.eq({
+      envUrl: undefined,
       dependencies: { bar: { version: '2.0.0' }, foo: { version: '1.0.0' } },
+      errors: [],
     })
   })
 
@@ -77,7 +107,9 @@ describe('lib/cloud/api', () => {
     const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'partial-dependencies-not-matching'), process.pid.toString())
 
     expect(information).to.deep.eq({
+      envUrl: undefined,
       dependencies: { bar: { version: '2.0.0' } },
+      errors: [],
     })
   })
 
@@ -90,8 +122,9 @@ describe('lib/cloud/api', () => {
       const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'partial-dependencies-matching'), pid.toString())
 
       expect(information).to.deep.eq({
-        ...(process.platform === 'win32' ? { envUrl: 'https://grandparent.com' } : {}),
+        envUrl: process.platform !== 'win32' ? 'https://grandparent.com' : undefined,
         dependencies: { foo: { version: '1.0.0' } },
+        errors: [],
       })
     })
 
@@ -103,8 +136,9 @@ describe('lib/cloud/api', () => {
       const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'partial-dependencies-matching'), pid.toString())
 
       expect(information).to.deep.eq({
-        ...(process.platform === 'win32' ? { envUrl: 'https://parent.com' } : {}),
+        envUrl: process.platform !== 'win32' ? 'https://parent.com' : undefined,
         dependencies: { foo: { version: '1.0.0' } },
+        errors: [],
       })
     })
 
@@ -116,8 +150,9 @@ describe('lib/cloud/api', () => {
       const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'partial-dependencies-matching'), pid.toString())
 
       expect(information).to.deep.eq({
-        ...(process.platform === 'win32' ? { envUrl: 'https://url.com' } : {}),
+        envUrl: process.platform !== 'win32' ? 'https://url.com' : undefined,
         dependencies: { foo: { version: '1.0.0' } },
+        errors: [],
       })
     })
 
@@ -130,8 +165,9 @@ describe('lib/cloud/api', () => {
       const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'partial-dependencies-matching'), pid.toString())
 
       expect(information).to.deep.eq({
-        ...(process.platform === 'win32' ? { envUrl: 'https://parent.com' } : {}),
+        envUrl: process.platform !== 'win32' ? 'https://parent.com' : undefined,
         dependencies: { foo: { version: '1.0.0' } },
+        errors: [],
       })
     })
 
@@ -141,7 +177,9 @@ describe('lib/cloud/api', () => {
       const information = await getEnvInformationForProjectRoot(path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'environment', 'partial-dependencies-matching'), pid.toString())
 
       expect(information).to.deep.eq({
+        envUrl: undefined,
         dependencies: { foo: { version: '1.0.0' } },
+        errors: [],
       })
     })
   })

From 29857daea40ec8e6a5862b77ca3472fb1f51f8dc Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Thu, 23 Feb 2023 11:27:33 -0600
Subject: [PATCH 38/52] pr comments

---
 packages/server/lib/cloud/api.ts              |  4 ++--
 packages/server/lib/cloud/environment.ts      |  3 +++
 .../server/test/integration/cypress_spec.js   |  2 +-
 packages/server/test/unit/cloud/api_spec.js   | 22 +++++++++----------
 .../server/test/unit/modes/record_spec.js     |  2 +-
 system-tests/__snapshots__/record_spec.js     | 18 +++++++--------
 system-tests/lib/serverStub.ts                |  2 +-
 system-tests/test/record_spec.js              | 20 ++++++++---------
 8 files changed, 38 insertions(+), 35 deletions(-)

diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts
index cfb33a038ecd..3000f7ec1851 100644
--- a/packages/server/lib/cloud/api.ts
+++ b/packages/server/lib/cloud/api.ts
@@ -286,7 +286,7 @@ module.exports = {
   createRun (options: CreateRunOptions) {
     const preflightOptions = _.pick(options, ['projectId', 'projectRoot', 'ciBuildId', 'browser', 'testingType', 'parallel', 'timeout'])
 
-    return this.postPreflight(preflightOptions)
+    return this.sendPreflight(preflightOptions)
     .then((result) => {
       const { warnings } = result
 
@@ -467,7 +467,7 @@ module.exports = {
     responseCache = {}
   },
 
-  postPreflight (preflightInfo) {
+  sendPreflight (preflightInfo) {
     return retryWithBackoff(async (attemptIndex) => {
       const { timeout, projectRoot } = preflightInfo
 
diff --git a/packages/server/lib/cloud/environment.ts b/packages/server/lib/cloud/environment.ts
index a9b46bfcb0d5..12f782faffc0 100644
--- a/packages/server/lib/cloud/environment.ts
+++ b/packages/server/lib/cloud/environment.ts
@@ -6,6 +6,7 @@ import resolvePackagePath from 'resolve-package-path'
 
 const execAsync = promisify(exec)
 
+// See https://whimsical.com/encryption-logic-BtJJkN7TxacK8kaHDgH1zM for more information on what this is doing
 const getProcessBranchForPid = async (pid: string) => {
   const { stdout } = await execAsync('ps -eo pid=,ppid=')
   const processTree = stdout.split('\n').reduce((acc, line) => {
@@ -36,6 +37,7 @@ interface GetCypressEnvUrlFromProcessBranch {
   }
 }
 
+// See https://whimsical.com/encryption-logic-BtJJkN7TxacK8kaHDgH1zM for more information on what this is doing
 const getCypressEnvUrlFromProcessBranch = async (pid: string): Promise => {
   let error: { name: string, message: string, stack: string } | undefined
   let envUrl: string | undefined
@@ -71,6 +73,7 @@ const getCypressEnvUrlFromProcessBranch = async (pid: string): Promise {
   let dependencies = {}
   let errors: { dependency?: string, name: string, message: string, stack: string }[] = []
diff --git a/packages/server/test/integration/cypress_spec.js b/packages/server/test/integration/cypress_spec.js
index 0f9db5bb911a..581c912b512d 100644
--- a/packages/server/test/integration/cypress_spec.js
+++ b/packages/server/test/integration/cypress_spec.js
@@ -1196,7 +1196,7 @@ describe('lib/cypress', () => {
     beforeEach(async function () {
       await clearCtx()
 
-      sinon.stub(api, 'postPreflight').resolves()
+      sinon.stub(api, 'sendPreflight').resolves()
       sinon.stub(api, 'createRun').resolves()
       const createInstanceStub = sinon.stub(api, 'createInstance')
 
diff --git a/packages/server/test/unit/cloud/api_spec.js b/packages/server/test/unit/cloud/api_spec.js
index 520993f3fd7e..23aa75edb489 100644
--- a/packages/server/test/unit/cloud/api_spec.js
+++ b/packages/server/test/unit/cloud/api_spec.js
@@ -218,7 +218,7 @@ describe('lib/cloud/api', () => {
     })
   })
 
-  context('.postPreflight', () => {
+  context('.sendPreflight', () => {
     let prodApi
 
     beforeEach(function () {
@@ -256,7 +256,7 @@ describe('lib/cloud/api', () => {
         },
       }))
 
-      return prodApi.postPreflight({ projectId: 'abc123' })
+      return prodApi.sendPreflight({ projectId: 'abc123' })
       .then((ret) => {
         expect(ret).to.deep.eq({ encrypt: true, apiUrl: `${API_PROD_BASEURL}/` })
       })
@@ -281,7 +281,7 @@ describe('lib/cloud/api', () => {
         },
       }))
 
-      return prodApi.postPreflight({ projectId: 'abc123' })
+      return prodApi.sendPreflight({ projectId: 'abc123' })
       .then((ret) => {
         scopeProxy.done()
         scopeApi.done()
@@ -308,7 +308,7 @@ describe('lib/cloud/api', () => {
         },
       }))
 
-      return prodApi.postPreflight({ projectId: 'abc123' })
+      return prodApi.sendPreflight({ projectId: 'abc123' })
       .then((ret) => {
         scopeProxy.done()
         scopeApi.done()
@@ -319,7 +319,7 @@ describe('lib/cloud/api', () => {
     it('sets timeout to 60 seconds', () => {
       sinon.stub(api.rp, 'post').resolves({})
 
-      return api.postPreflight({})
+      return api.sendPreflight({})
       .then(() => {
         expect(api.rp.post).to.be.calledWithMatch({ timeout: 60000 })
       })
@@ -332,7 +332,7 @@ describe('lib/cloud/api', () => {
         .delayConnection(5000)
         .reply(200, {})
 
-        return api.postPreflight({
+        return api.sendPreflight({
           timeout: 100,
         })
         .then(() => {
@@ -350,7 +350,7 @@ describe('lib/cloud/api', () => {
         const scopeApi = preflightNock(API_PROD_BASEURL)
         .replyWithError('2nd request error')
 
-        return prodApi.postPreflight({ projectId: 'abc123' })
+        return prodApi.sendPreflight({ projectId: 'abc123' })
         .then(() => {
           throw new Error('should have thrown here')
         })
@@ -373,7 +373,7 @@ describe('lib/cloud/api', () => {
         const scopeApi = preflightNock(API_PROD_BASEURL)
         .reply(500)
 
-        return prodApi.postPreflight({ projectId: 'abc123' })
+        return prodApi.sendPreflight({ projectId: 'abc123' })
         .then(() => {
           throw new Error('should have thrown here')
         })
@@ -397,7 +397,7 @@ describe('lib/cloud/api', () => {
           'Content-Type': 'text/html',
         })
 
-        return prodApi.postPreflight({ projectId: 'abc123' })
+        return prodApi.sendPreflight({ projectId: 'abc123' })
         .then(() => {
           throw new Error('should have thrown here')
         })
@@ -424,7 +424,7 @@ describe('lib/cloud/api', () => {
         const scopeApi = preflightNock(API_PROD_BASEURL)
         .reply(201, 'very encrypted and secure string')
 
-        return prodApi.postPreflight({ projectId: 'abc123' })
+        return prodApi.sendPreflight({ projectId: 'abc123' })
         .then(() => {
           throw new Error('should have thrown here')
         })
@@ -447,7 +447,7 @@ describe('lib/cloud/api', () => {
         const scopeApi = preflightNock(API_PROD_BASEURL)
         .reply(201)
 
-        return prodApi.postPreflight({ projectId: 'abc123' })
+        return prodApi.sendPreflight({ projectId: 'abc123' })
         .then(() => {
           throw new Error('should have thrown here')
         })
diff --git a/packages/server/test/unit/modes/record_spec.js b/packages/server/test/unit/modes/record_spec.js
index 8c13bd0c1eae..dd08db98a9a5 100644
--- a/packages/server/test/unit/modes/record_spec.js
+++ b/packages/server/test/unit/modes/record_spec.js
@@ -17,7 +17,7 @@ const initialEnv = _.clone(process.env)
 // tested as an e2e/record_spec
 describe('lib/modes/record', () => {
   beforeEach(() => {
-    sinon.stub(api, 'postPreflight').callsFake(async () => {
+    sinon.stub(api, 'sendPreflight').callsFake(async () => {
       api.setPreflightResult({ encrypt: false })
     })
   })
diff --git a/system-tests/__snapshots__/record_spec.js b/system-tests/__snapshots__/record_spec.js
index ab67202a8927..235a37fa3590 100644
--- a/system-tests/__snapshots__/record_spec.js
+++ b/system-tests/__snapshots__/record_spec.js
@@ -2543,7 +2543,7 @@ Available browsers found on your system are:
 - browser3
 `
 
-exports['e2e record api interaction errors postPreflight [F1] fails on 500 status codes with empty body after retrying 1'] = `
+exports['e2e record api interaction errors sendPreflight [F1] fails on 500 status codes with empty body after retrying 1'] = `
 We encountered an unexpected error communicating with our servers.
 
 StatusCodeError: 500 - "Internal Server Error"
@@ -2561,7 +2561,7 @@ The --ciBuildId flag you passed was: ciBuildId123
 
 `
 
-exports['e2e record api interaction errors postPreflight [F2] fails on 404 status codes with JSON body without retrying 1'] = `
+exports['e2e record api interaction errors sendPreflight [F2] fails on 404 status codes with JSON body without retrying 1'] = `
 We could not find a Cypress Cloud project with the projectId: pid123
 
 This projectId came from your cypress-with-project-id.config.js file or an environment variable.
@@ -2576,7 +2576,7 @@ https://on.cypress.io/cloud
 
 `
 
-exports['e2e record api interaction errors postPreflight [F2] fails on 404 status codes without JSON body without retrying 1'] = `
+exports['e2e record api interaction errors sendPreflight [F2] fails on 404 status codes without JSON body without retrying 1'] = `
 We could not find a Cypress Cloud project with the projectId: pid123
 
 This projectId came from your cypress-with-project-id.config.js file or an environment variable.
@@ -2591,7 +2591,7 @@ https://on.cypress.io/cloud
 
 `
 
-exports['e2e record api interaction errors postPreflight [F5] fails on OK status codes with invalid unencrypted data without retrying 1'] = `
+exports['e2e record api interaction errors sendPreflight [F5] fails on OK status codes with invalid unencrypted data without retrying 1'] = `
 We encountered an unexpected error communicating with our servers.
 
 DecryptionError: JWE Recipients missing or incorrect type
@@ -2603,7 +2603,7 @@ The --ciBuildId flag you passed was: ciBuildId123
 
 `
 
-exports['e2e record api interaction errors postPreflight [F6] fails on OK status codes with empty body without retrying 1'] = `
+exports['e2e record api interaction errors sendPreflight [F6] fails on OK status codes with empty body without retrying 1'] = `
 We encountered an unexpected error communicating with our servers.
 
 DecryptionError: General JWE must be an object
@@ -2615,7 +2615,7 @@ The --ciBuildId flag you passed was: ciBuildId123
 
 `
 
-exports['e2e record api interaction errors postPreflight [F3] fails on 412 status codes when request is invalid 1'] = `
+exports['e2e record api interaction errors sendPreflight [F3] fails on 412 status codes when request is invalid 1'] = `
 Recording this run failed. The request was invalid.
 
 Recording is not working
@@ -2634,7 +2634,7 @@ Request Sent:
 
 `
 
-exports['e2e record api interaction errors postPreflight preflight failure: warning message renders preflight warning messages prior to run warnings 1'] = `
+exports['e2e record api interaction errors sendPreflight preflight failure: warning message renders preflight warning messages prior to run warnings 1'] = `
 Warning from Cypress Cloud: 
 
 ----------------------------------------------------------------------
@@ -2720,7 +2720,7 @@ https://on.cypress.io/dashboard/organizations/org-id-1234/billing
 
 `
 
-exports['e2e record api interaction errors postPreflight [F1] fails on request socket errors after retrying 1'] = `
+exports['e2e record api interaction errors sendPreflight [F1] fails on request socket errors after retrying 1'] = `
 We encountered an unexpected error communicating with our servers.
 
 RequestError: Error: socket hang up
@@ -2738,7 +2738,7 @@ The --ciBuildId flag you passed was: ciBuildId123
 
 `
 
-exports['e2e record api interaction errors postPreflight [F4] fails on 422 status codes even when encryption is off 1'] = `
+exports['e2e record api interaction errors sendPreflight [F4] fails on 422 status codes even when encryption is off 1'] = `
 We encountered an unexpected error communicating with our servers.
 
 StatusCodeError: 422
diff --git a/system-tests/lib/serverStub.ts b/system-tests/lib/serverStub.ts
index 9d3cbc0d425c..8d333e6b4a96 100644
--- a/system-tests/lib/serverStub.ts
+++ b/system-tests/lib/serverStub.ts
@@ -70,7 +70,7 @@ export const encryptBody = async (req, res, body) => {
 }
 
 export const routeHandlers = {
-  postPreflight: {
+  sendPreflight: {
     method: 'post',
     url: '/preflight',
     res: async (req, res) => {
diff --git a/system-tests/test/record_spec.js b/system-tests/test/record_spec.js
index f1dc7ca60753..003f73f4426e 100644
--- a/system-tests/test/record_spec.js
+++ b/system-tests/test/record_spec.js
@@ -1614,10 +1614,10 @@ describe('e2e record', () => {
       })
     })
 
-    describe('postPreflight', () => {
+    describe('sendPreflight', () => {
       describe('[F1]', () => {
         setupStubbedServer(createRoutes({
-          postPreflight: {
+          sendPreflight: {
             res (req, res) {
               return req.socket.destroy(new Error('killed'))
             },
@@ -1644,7 +1644,7 @@ describe('e2e record', () => {
 
       describe('[F1]', () => {
         setupStubbedServer(createRoutes({
-          postPreflight: {
+          sendPreflight: {
             res (req, res) {
               return res.sendStatus(500)
             },
@@ -1671,7 +1671,7 @@ describe('e2e record', () => {
 
       describe('[F2]', () => {
         setupStubbedServer(createRoutes({
-          postPreflight: {
+          sendPreflight: {
             res (req, res) {
               return res
               .status(404)
@@ -1700,7 +1700,7 @@ describe('e2e record', () => {
 
       describe('[F2]', () => {
         setupStubbedServer(createRoutes({
-          postPreflight: {
+          sendPreflight: {
             res (req, res) {
               return res.sendStatus(404)
             },
@@ -1727,7 +1727,7 @@ describe('e2e record', () => {
 
       describe('[F3]', () => {
         setupStubbedServer(createRoutes({
-          postPreflight: {
+          sendPreflight: {
             res: async (req, res) => {
               return res.status(412).json(await encryptBody(req, res, {
                 message: 'Recording is not working',
@@ -1762,7 +1762,7 @@ describe('e2e record', () => {
 
       describe('[F4]', () => {
         setupStubbedServer(createRoutes({
-          postPreflight: {
+          sendPreflight: {
             res: async (req, res) => {
               return res.status(422).json({
                 message: 'something broke',
@@ -1791,7 +1791,7 @@ describe('e2e record', () => {
 
       describe('[F5]', () => {
         setupStubbedServer(createRoutes({
-          postPreflight: {
+          sendPreflight: {
             res (req, res) {
               return res
               .status(201)
@@ -1820,7 +1820,7 @@ describe('e2e record', () => {
 
       describe('[F6]', () => {
         setupStubbedServer(createRoutes({
-          postPreflight: {
+          sendPreflight: {
             res (req, res) {
               return res.sendStatus(200)
             },
@@ -1847,7 +1847,7 @@ describe('e2e record', () => {
 
       describe('preflight failure: warning message', () => {
         const mockServer = setupStubbedServer(createRoutes({
-          postPreflight: {
+          sendPreflight: {
             res: async (req, res) => {
               return res.json(await encryptBody(req, res, {
                 encrypt: true,

From 3a54b3d628db1da50d4bb4992acc3084b48ce45a Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Thu, 23 Feb 2023 15:41:25 -0600
Subject: [PATCH 39/52] isolate cypress_env_dependencies to just the built
 binary

---
 .circleci/workflows.yml | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml
index e48fc5a987be..06d30f091416 100644
--- a/.circleci/workflows.yml
+++ b/.circleci/workflows.yml
@@ -2598,6 +2598,7 @@ linux-x64-workflow: &linux-x64-workflow
         context:
           - test-runner:upload
           - test-runner:commit-status-checks
+          - test-runner:build-binary
         requires:
           - build
     # various testing scenarios, like building full binary
@@ -2698,6 +2699,7 @@ linux-arm64-workflow: &linux-arm64-workflow
         context:
           - test-runner:upload
           - test-runner:commit-status-checks
+          - test-runner:build-binary
         executor: linux-arm64
         resource_class: arm.medium
         requires:
@@ -2738,6 +2740,7 @@ darwin-x64-workflow: &darwin-x64-workflow
           - test-runner:sign-mac-binary
           - test-runner:upload
           - test-runner:commit-status-checks
+          - test-runner:build-binary
         executor: mac
         resource_class: macos.x86.medium.gen2
         requires:
@@ -2789,6 +2792,7 @@ darwin-arm64-workflow: &darwin-arm64-workflow
           - test-runner:sign-mac-binary
           - test-runner:upload
           - test-runner:commit-status-checks
+          - test-runner:build-binary
         executor: darwin-arm64
         resource_class: cypress-io/latest_m1
         requires:
@@ -2861,6 +2865,7 @@ windows-workflow: &windows-workflow
           - test-runner:sign-windows-binary
           - test-runner:upload
           - test-runner:commit-status-checks
+          - test-runner:build-binary
         requires:
           - windows-build
     - test-binary-against-kitchensink-chrome:

From ea075e7e50a31f35de8b60227b88f77e049654c3 Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Thu, 23 Feb 2023 16:52:52 -0600
Subject: [PATCH 40/52] fix tests

---
 guides/error-handling.md                        | 2 +-
 system-tests/__snapshots__/record_spec.js       | 2 +-
 system-tests/__snapshots__/web_security_spec.js | 2 +-
 system-tests/lib/system-tests.ts                | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/guides/error-handling.md b/guides/error-handling.md
index 3035e33c3259..4ae4daf3d021 100644
--- a/guides/error-handling.md
+++ b/guides/error-handling.md
@@ -68,7 +68,7 @@ CANNOT_TRASH_ASSETS: (arg1: string) => {
   return errTemplate`\
       Warning: We failed to trash the existing run results.
 
-      This error will not alter the exit code.
+      This error will not affect or change the exit code.
 
       ${details(arg1)}`
 },
diff --git a/system-tests/__snapshots__/record_spec.js b/system-tests/__snapshots__/record_spec.js
index 235a37fa3590..147ec4847f60 100644
--- a/system-tests/__snapshots__/record_spec.js
+++ b/system-tests/__snapshots__/record_spec.js
@@ -1133,7 +1133,7 @@ Details:
 
   (Screenshots)
 
-
+  -  /XXX/XXX/XXX/cypress/screenshots/record_pass.cy.js/yay it passes.png                 (400x1022)
 
   (Uploading Results)
 
diff --git a/system-tests/__snapshots__/web_security_spec.js b/system-tests/__snapshots__/web_security_spec.js
index 80b65be410f2..4aacbbcd74f9 100644
--- a/system-tests/__snapshots__/web_security_spec.js
+++ b/system-tests/__snapshots__/web_security_spec.js
@@ -213,7 +213,7 @@ This option will not have an effect in Firefox. Tests that rely on web security
 
 Warning: We failed processing this video.
 
-This error will not alter the exit code.
+This error will not affect or change the exit code.
 
 TimeoutError: operation timed out
       [stack trace lines]
diff --git a/system-tests/lib/system-tests.ts b/system-tests/lib/system-tests.ts
index fa5ae7162b34..ada6c36d09d6 100644
--- a/system-tests/lib/system-tests.ts
+++ b/system-tests/lib/system-tests.ts
@@ -308,7 +308,7 @@ Bluebird.config({
 const diffRe = /Difference\n-{10}\n([\s\S]*)\n-{19}\nSaved snapshot text/m
 const expectedAddedVideoSnapshotLines = [
   'Warning: We failed processing this video.',
-  'This error will not alter the exit code.',
+  'This error will not affect or change the exit code.',
   'TimeoutError: operation timed out',
   '[stack trace lines]',
 ]

From 6780197baa500bf54e6c3da19d0608233ce957c7 Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Thu, 23 Feb 2023 17:35:35 -0600
Subject: [PATCH 41/52] fix test - run ci

---
 system-tests/__snapshots__/record_spec.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/system-tests/__snapshots__/record_spec.js b/system-tests/__snapshots__/record_spec.js
index 147ec4847f60..6a90001e1a65 100644
--- a/system-tests/__snapshots__/record_spec.js
+++ b/system-tests/__snapshots__/record_spec.js
@@ -1135,6 +1135,7 @@ Details:
 
   -  /XXX/XXX/XXX/cypress/screenshots/record_pass.cy.js/yay it passes.png                 (400x1022)
 
+
   (Uploading Results)
 
   - Done Uploading (1/1) /foo/bar/.projects/e2e/cypress/screenshots/record_pass.cy.js/yay it passes.png

From 4e7c7bf6321cf7add134d935231c7e80b79b3b68 Mon Sep 17 00:00:00 2001
From: Tim Griesser 
Date: Thu, 23 Feb 2023 20:58:19 -0500
Subject: [PATCH 42/52] Updating logic for 422

---
 packages/server/lib/cloud/api.ts | 9 ++-------
 1 file changed, 2 insertions(+), 7 deletions(-)

diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts
index 3000f7ec1851..16706287147b 100644
--- a/packages/server/lib/cloud/api.ts
+++ b/packages/server/lib/cloud/api.ts
@@ -88,7 +88,6 @@ const rp = request.defaults((params: CypressRequestOptions, callback) => {
     if (params.encrypt === true || params.encrypt === 'always') {
       const { secretKey, jwe } = await enc.encryptRequest(params)
 
-      // TODO: double check the logic below here with @tgriesser
       params.transform = async function (body, response) {
         const { statusCode } = response
 
@@ -96,15 +95,13 @@ const rp = request.defaults((params: CypressRequestOptions, callback) => {
         if (response.headers['x-cypress-encrypted'] || params.encrypt === 'always') {
           let decryptedBody
 
-          // TODO: if body is null/undefined throw a custom error
-
           try {
             decryptedBody = await enc.decryptResponse(body, secretKey)
           } catch (e) {
             // we failed decrypting the response...
 
             // if status code is >=500 or 404 just return body
-            if (statusCode >= 500 || statusCode === 404 || statusCode === 422) {
+            if (statusCode >= 500 || statusCode === 404) {
               return body
             }
 
@@ -113,7 +110,6 @@ const rp = request.defaults((params: CypressRequestOptions, callback) => {
 
           // If we've hit an encrypted payload error case, we need to re-constitute the error
           // as it would happen normally, with the body as an error property
-          // TODO: need to look harder at this to better understand why its necessary
           if (response.statusCode > 400) {
             throw new RequestErrors.StatusCodeError(response.statusCode, decryptedBody, {}, decryptedBody)
           }
@@ -184,8 +180,8 @@ const retryWithBackoff = (fn) => {
         return attempt(retryIndex)
       })
     })
-    // TODO: look at this too
     .catch(RequestErrors.TransformError, (err) => {
+      // Unroll the error thrown from within the transform
       throw err.cause
     })
   }
@@ -258,7 +254,6 @@ module.exports = {
     }
   },
 
-  // TODO: i think we can remove this function
   resetPreflightResult () {
     recordRoutes = apiRoutes
     preflightResult = {

From a9e07e0cfcece0dba556c5cba57f8c5401a4ccd9 Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Thu, 23 Feb 2023 20:59:38 -0600
Subject: [PATCH 43/52] updates to CYPRESS_ENV_DEPENDENCIES structure

---
 packages/server/lib/cloud/environment.ts      | 35 ++++++++++++++-----
 .../test/unit/cloud/environment_spec.ts       |  8 ++---
 2 files changed, 29 insertions(+), 14 deletions(-)

diff --git a/packages/server/lib/cloud/environment.ts b/packages/server/lib/cloud/environment.ts
index 12f782faffc0..6b453eb463d8 100644
--- a/packages/server/lib/cloud/environment.ts
+++ b/packages/server/lib/cloud/environment.ts
@@ -73,25 +73,31 @@ const getCypressEnvUrlFromProcessBranch = async (pid: string): Promise {
   let dependencies = {}
   let errors: { dependency?: string, name: string, message: string, stack: string }[] = []
   let envDependenciesVar = process.env.CYPRESS_ENV_DEPENDENCIES
   let envUrl = process.env.CYPRESS_API_URL
-  let processTreePromise: Promise = !envUrl ? getCypressEnvUrlFromProcessBranch(pid) : Promise.resolve({})
+  let checkProcessTree
 
   if (envDependenciesVar) {
-    const envDependenciesInformation = JSON.parse(base64Url.decode(envDependenciesVar)) as Record
+    const envDependenciesInformation = JSON.parse(base64Url.decode(envDependenciesVar)) as DependencyInformation
 
     const packageToJsonMapping: Record = {}
 
-    Object.entries(envDependenciesInformation).map(([dependency, { processTreeCheckRequirement }]) => {
+    envDependenciesInformation.maybeCheckProcessTreeIfPresent.forEach((dependency) => {
       try {
         const packageJsonPath = resolvePackagePath(dependency, projectRoot)
 
         if (packageJsonPath) {
           packageToJsonMapping[dependency] = packageJsonPath
+          checkProcessTree = true
         }
       } catch (error) {
         errors.push({
@@ -101,15 +107,28 @@ const getEnvInformationForProjectRoot = async (projectRoot: string, pid: string)
           stack: error.stack,
         })
       }
-      if (processTreeCheckRequirement === 'presence required' && !packageToJsonMapping[dependency]) {
-        processTreePromise = Promise.resolve({})
-      } else if (processTreeCheckRequirement === 'absence required' && packageToJsonMapping[dependency]) {
-        processTreePromise = Promise.resolve({})
+    })
+
+    envDependenciesInformation.neverCheckProcessTreeIfPresent.forEach((dependency) => {
+      try {
+        const packageJsonPath = resolvePackagePath(dependency, projectRoot)
+
+        if (packageJsonPath) {
+          packageToJsonMapping[dependency] = packageJsonPath
+          checkProcessTree = false
+        }
+      } catch (error) {
+        errors.push({
+          dependency,
+          name: error.name,
+          message: error.message,
+          stack: error.stack,
+        })
       }
     })
 
     const [{ envUrl: processTreeEnvUrl, error: processTreeError }] = await Promise.all([
-      processTreePromise,
+      checkProcessTree ? getCypressEnvUrlFromProcessBranch(pid) : { envUrl: undefined, error: undefined },
       ...Object.entries(packageToJsonMapping).map(async ([dependency, packageJsonPath]) => {
         try {
           const packageVersion = (await fs.readJSON(packageJsonPath)).version
diff --git a/packages/server/test/unit/cloud/environment_spec.ts b/packages/server/test/unit/cloud/environment_spec.ts
index a8de3b4f5c99..302f36ce8c71 100644
--- a/packages/server/test/unit/cloud/environment_spec.ts
+++ b/packages/server/test/unit/cloud/environment_spec.ts
@@ -10,12 +10,8 @@ describe('lib/cloud/api', () => {
   beforeEach(() => {
     delete process.env.CYPRESS_API_URL
     process.env.CYPRESS_ENV_DEPENDENCIES = base64url.encode(JSON.stringify({
-      'foo': {
-        processTreeCheckRequirement: 'presence required',
-      },
-      'bar': {
-        processTreeCheckRequirement: 'absence required',
-      },
+      maybeCheckProcessTreeIfPresent: ['foo'],
+      neverCheckProcessTreeIfPresent: ['bar'],
     }))
   })
 

From d2bda852a9ec09093fd01f470a93239dac42bc20 Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Thu, 23 Feb 2023 21:01:20 -0600
Subject: [PATCH 44/52] run ci


From c1b33573b30da1a5b6f339f0351f9d276b308a87 Mon Sep 17 00:00:00 2001
From: Brian Mann 
Date: Fri, 24 Feb 2023 10:07:24 -0500
Subject: [PATCH 45/52] fix test for 422 error

---
 system-tests/__snapshots__/record_spec.js | 10 ++--------
 1 file changed, 2 insertions(+), 8 deletions(-)

diff --git a/system-tests/__snapshots__/record_spec.js b/system-tests/__snapshots__/record_spec.js
index 6a90001e1a65..54fc701d8b28 100644
--- a/system-tests/__snapshots__/record_spec.js
+++ b/system-tests/__snapshots__/record_spec.js
@@ -2742,17 +2742,11 @@ The --ciBuildId flag you passed was: ciBuildId123
 exports['e2e record api interaction errors sendPreflight [F4] fails on 422 status codes even when encryption is off 1'] = `
 We encountered an unexpected error communicating with our servers.
 
-StatusCodeError: 422
-
-{
-  "message": "something broke"
-}
+DecryptionError: JWE Recipients missing or incorrect type
 
-There is likely something wrong with the request.
+Because you passed the --parallel flag, this run cannot proceed because it requires a valid response from our servers.
 
-The --tag flag you passed was: nightly
 The --group flag you passed was: foo
-The --parallel flag you passed was: true
 The --ciBuildId flag you passed was: ciBuildId123
 
 `

From d6681a550ab3e7333d72d9dc365174a056cba493 Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Fri, 24 Feb 2023 09:16:41 -0600
Subject: [PATCH 46/52] Update CHANGELOG.md

---
 cli/CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md
index c7f3ad48bd28..bc9a5cff83fe 100644
--- a/cli/CHANGELOG.md
+++ b/cli/CHANGELOG.md
@@ -11,6 +11,7 @@ _Released 02/28/2023 (PENDING)_
 
 **Bugfixes:**
 
+- Improved various error messages around interactions with the Cypress cloud and fixed various bugs when recording to the cloud. Fixed in [#25837](/~https://github.com/cypress-io/cypress/pull/25837)
 - Fixed an issue where cookies were being duplicated with the same hostname, but a prepended dot. Fixed an issue where cookies may not be expiring correctly. Fixes [#25174](/~https://github.com/cypress-io/cypress/issues/25174), [#25205](/~https://github.com/cypress-io/cypress/issues/25205) and [#25495](/~https://github.com/cypress-io/cypress/issues/25495).
 - Fixed an issue where cookies weren't being synced when the application was stable. Fixed in [#25855](/~https://github.com/cypress-io/cypress/pull/25855). Fixes [#25835](/~https://github.com/cypress-io/cypress/issues/25835).
 - Added missing TypeScript type definitions for the [`cy.reload()`](https://docs.cypress.io/api/commands/reload) command. Addressed in [#25779](/~https://github.com/cypress-io/cypress/pull/25779).

From 878aa1e60721fdf7090c79a18453a91a0d284a16 Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Fri, 24 Feb 2023 10:00:37 -0600
Subject: [PATCH 47/52] Update CHANGELOG.md

---
 cli/CHANGELOG.md | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md
index bc9a5cff83fe..b07a9dac557e 100644
--- a/cli/CHANGELOG.md
+++ b/cli/CHANGELOG.md
@@ -8,10 +8,11 @@ _Released 02/28/2023 (PENDING)_
 - It is now possible to set `hostOnly` cookies with [`cy.setCookie()`](https://docs.cypress.io/api/commands/setcookie) for a given domain. Addresses [#16856](/~https://github.com/cypress-io/cypress/issues/16856) and [#17527](/~https://github.com/cypress-io/cypress/issues/17527).
 - Added a Public API for third party component libraries to define a Framework Definition, embedding their library into the Cypress onboarding workflow. Learn more [here](https://docs.cypress.io/guides/component-testing/third-party-definitions). Implemented in [#25780](/~https://github.com/cypress-io/cypress/pull/25780) and closes [#25638](/~https://github.com/cypress-io/cypress/issues/25638).
 - Added a Debug Page tutorial slideshow for projects that are not connected to Cypress Cloud. Addresses [#25768](/~https://github.com/cypress-io/cypress/issues/25768).
+- Improved various error message around interactions with the Cypress cloud. Implemented in [#25837](/~https://github.com/cypress-io/cypress/pull/25837)
 
 **Bugfixes:**
 
-- Improved various error messages around interactions with the Cypress cloud and fixed various bugs when recording to the cloud. Fixed in [#25837](/~https://github.com/cypress-io/cypress/pull/25837)
+- Fixed various bugs when recording to the cloud. Fixed in [#25837](/~https://github.com/cypress-io/cypress/pull/25837)
 - Fixed an issue where cookies were being duplicated with the same hostname, but a prepended dot. Fixed an issue where cookies may not be expiring correctly. Fixes [#25174](/~https://github.com/cypress-io/cypress/issues/25174), [#25205](/~https://github.com/cypress-io/cypress/issues/25205) and [#25495](/~https://github.com/cypress-io/cypress/issues/25495).
 - Fixed an issue where cookies weren't being synced when the application was stable. Fixed in [#25855](/~https://github.com/cypress-io/cypress/pull/25855). Fixes [#25835](/~https://github.com/cypress-io/cypress/issues/25835).
 - Added missing TypeScript type definitions for the [`cy.reload()`](https://docs.cypress.io/api/commands/reload) command. Addressed in [#25779](/~https://github.com/cypress-io/cypress/pull/25779).

From 802f19373fc7af205b04406440a08ba34f1e6a27 Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Fri, 24 Feb 2023 11:01:50 -0600
Subject: [PATCH 48/52] PR comments

---
 .circleci/workflows.yml                       | 12 ++++
 packages/server/lib/cloud/environment.ts      | 55 +++++++------------
 .../fixtures/cloud/environment/.gitignore     |  2 +-
 .../environment/test-project/package.json     |  2 +-
 4 files changed, 35 insertions(+), 36 deletions(-)

diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml
index 06d30f091416..22f8d01153ce 100644
--- a/.circleci/workflows.yml
+++ b/.circleci/workflows.yml
@@ -2717,6 +2717,12 @@ linux-arm64-workflow: &linux-arm64-workflow
         resource_class: arm.medium
         requires:
           - linux-arm64-build
+    - server-unit-tests-cloud-environment:
+        name: linux-arm64-server-unit-tests-cloud-environment
+        executor: linux-arm64
+        resource_class: arm.medium
+        requires:
+          - linux-arm64-build
 
 darwin-x64-workflow: &darwin-x64-workflow
   jobs:
@@ -2810,6 +2816,12 @@ darwin-arm64-workflow: &darwin-arm64-workflow
         resource_class: cypress-io/latest_m1
         requires:
           - darwin-arm64-build
+    - server-unit-tests-cloud-environment:
+        name: darwin-arm64-server-unit-tests-cloud-environment
+        executor: darwin-arm64
+        resource_class: cypress-io/latest_m1
+        requires:
+          - darwin-arm64-build
 
 windows-workflow: &windows-workflow
   jobs:
diff --git a/packages/server/lib/cloud/environment.ts b/packages/server/lib/cloud/environment.ts
index 6b453eb463d8..f86a2609b0d8 100644
--- a/packages/server/lib/cloud/environment.ts
+++ b/packages/server/lib/cloud/environment.ts
@@ -82,50 +82,37 @@ interface DependencyInformation {
 const getEnvInformationForProjectRoot = async (projectRoot: string, pid: string) => {
   let dependencies = {}
   let errors: { dependency?: string, name: string, message: string, stack: string }[] = []
-  let envDependenciesVar = process.env.CYPRESS_ENV_DEPENDENCIES
+  let envDependencies = process.env.CYPRESS_ENV_DEPENDENCIES
   let envUrl = process.env.CYPRESS_API_URL
   let checkProcessTree
 
-  if (envDependenciesVar) {
-    const envDependenciesInformation = JSON.parse(base64Url.decode(envDependenciesVar)) as DependencyInformation
+  if (envDependencies) {
+    const envDependenciesInformation = JSON.parse(base64Url.decode(envDependencies)) as DependencyInformation
 
     const packageToJsonMapping: Record = {}
 
-    envDependenciesInformation.maybeCheckProcessTreeIfPresent.forEach((dependency) => {
-      try {
-        const packageJsonPath = resolvePackagePath(dependency, projectRoot)
+    const processDependency = ({ checkOnFound }) => {
+      return (dependency) => {
+        try {
+          const packageJsonPath = resolvePackagePath(dependency, projectRoot)
 
-        if (packageJsonPath) {
-          packageToJsonMapping[dependency] = packageJsonPath
-          checkProcessTree = true
+          if (packageJsonPath) {
+            packageToJsonMapping[dependency] = packageJsonPath
+            checkProcessTree = checkOnFound
+          }
+        } catch (error) {
+          errors.push({
+            dependency,
+            name: error.name,
+            message: error.message,
+            stack: error.stack,
+          })
         }
-      } catch (error) {
-        errors.push({
-          dependency,
-          name: error.name,
-          message: error.message,
-          stack: error.stack,
-        })
       }
-    })
-
-    envDependenciesInformation.neverCheckProcessTreeIfPresent.forEach((dependency) => {
-      try {
-        const packageJsonPath = resolvePackagePath(dependency, projectRoot)
+    }
 
-        if (packageJsonPath) {
-          packageToJsonMapping[dependency] = packageJsonPath
-          checkProcessTree = false
-        }
-      } catch (error) {
-        errors.push({
-          dependency,
-          name: error.name,
-          message: error.message,
-          stack: error.stack,
-        })
-      }
-    })
+    envDependenciesInformation.maybeCheckProcessTreeIfPresent.forEach(processDependency({ checkOnFound: true }))
+    envDependenciesInformation.neverCheckProcessTreeIfPresent.forEach(processDependency({ checkOnFound: false }))
 
     const [{ envUrl: processTreeEnvUrl, error: processTreeError }] = await Promise.all([
       checkProcessTree ? getCypressEnvUrlFromProcessBranch(pid) : { envUrl: undefined, error: undefined },
diff --git a/packages/server/test/support/fixtures/cloud/environment/.gitignore b/packages/server/test/support/fixtures/cloud/environment/.gitignore
index 736e8ae58ad8..cf4bab9ddde9 100644
--- a/packages/server/test/support/fixtures/cloud/environment/.gitignore
+++ b/packages/server/test/support/fixtures/cloud/environment/.gitignore
@@ -1 +1 @@
-!node_modules
\ No newline at end of file
+!node_modules
diff --git a/packages/server/test/support/fixtures/cloud/environment/test-project/package.json b/packages/server/test/support/fixtures/cloud/environment/test-project/package.json
index eb235e08126c..8ca713b3fa7e 100644
--- a/packages/server/test/support/fixtures/cloud/environment/test-project/package.json
+++ b/packages/server/test/support/fixtures/cloud/environment/test-project/package.json
@@ -2,4 +2,4 @@
   "name": "test-project",
   "version": "1.0.0",
   "type": "module"
-}
\ No newline at end of file
+}

From d6086a654efaa542b167f94f17816356262f4277 Mon Sep 17 00:00:00 2001
From: Ryan Manuel 
Date: Fri, 24 Feb 2023 12:14:23 -0600
Subject: [PATCH 49/52] fix build

---
 .circleci/workflows.yml | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml
index 22f8d01153ce..fc6e335d4d8c 100644
--- a/.circleci/workflows.yml
+++ b/.circleci/workflows.yml
@@ -1499,6 +1499,12 @@ jobs:
     parallelism: 1
     steps:
       - restore_cached_workspace
+      # TODO: Remove this once we switch off self-hosted M1 runners
+      - when:
+          condition:
+            equal: [ *darwin-arm64-executor, << parameters.executor >> ]
+          steps:
+            - run: rm -f /tmp/cypress/junit/*
       - run: yarn workspace @packages/server test-unit cloud/environment_spec.ts
       - verify-mocha-results:
           expectedResultCount: 1

From 2c5094cbeba58c2236df28e45c61ea89caa44fd8 Mon Sep 17 00:00:00 2001
From: Brian Mann 
Date: Fri, 24 Feb 2023 14:34:59 -0500
Subject: [PATCH 50/52] retry on all 5xx errors

-throw proper status code error
- properly unroll transform errors
- add tests
---
 packages/server/lib/cloud/api.ts          | 30 ++++++++++---------
 system-tests/__snapshots__/record_spec.js | 32 ++++++++++++++++++---
 system-tests/test/record_spec.js          | 35 +++++++++++++++++++++--
 3 files changed, 76 insertions(+), 21 deletions(-)

diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts
index 16706287147b..0f5b8a8da71c 100644
--- a/packages/server/lib/cloud/api.ts
+++ b/packages/server/lib/cloud/api.ts
@@ -13,10 +13,11 @@ const errors = require('../errors')
 const { apiUrl, apiRoutes, makeRoutes } = require('./routes')
 
 import Bluebird from 'bluebird'
-import type { OptionsWithUrl } from 'request-promise'
+import { getText } from '../util/status_code'
 import * as enc from './encryption'
 import getEnvInformationForProjectRoot from './environment'
 
+import type { OptionsWithUrl } from 'request-promise'
 const THIRTY_SECONDS = humanInterval('30 seconds')
 const SIXTY_SECONDS = humanInterval('60 seconds')
 const TWO_MINUTES = humanInterval('2 minutes')
@@ -90,6 +91,11 @@ const rp = request.defaults((params: CypressRequestOptions, callback) => {
 
       params.transform = async function (body, response) {
         const { statusCode } = response
+        const options = this // request promise options
+
+        const throwStatusCodeErrWithResp = (message, responseBody) => {
+          throw new RequestErrors.StatusCodeError(response.statusCode, message, options, responseBody)
+        }
 
         // response is valid and we are encrypting
         if (response.headers['x-cypress-encrypted'] || params.encrypt === 'always') {
@@ -100,9 +106,10 @@ const rp = request.defaults((params: CypressRequestOptions, callback) => {
           } catch (e) {
             // we failed decrypting the response...
 
-            // if status code is >=500 or 404 just return body
+            // if status code is >=500 or 404 remove body
             if (statusCode >= 500 || statusCode === 404) {
-              return body
+              // remove server responses and replace with basic status code text
+              throwStatusCodeErrWithResp(getText(statusCode), body)
             }
 
             throw new DecryptionError(e.message)
@@ -111,7 +118,7 @@ const rp = request.defaults((params: CypressRequestOptions, callback) => {
           // If we've hit an encrypted payload error case, we need to re-constitute the error
           // as it would happen normally, with the body as an error property
           if (response.statusCode > 400) {
-            throw new RequestErrors.StatusCodeError(response.statusCode, decryptedBody, {}, decryptedBody)
+            throwStatusCodeErrWithResp(decryptedBody, decryptedBody)
           }
 
           return decryptedBody
@@ -155,6 +162,10 @@ const retryWithBackoff = (fn) => {
   const attempt = (retryIndex) => {
     return Bluebird
     .try(() => fn(retryIndex))
+    .catch(RequestErrors.TransformError, (err) => {
+      // Unroll the error thrown from within the transform
+      throw err.cause
+    })
     .catch(isRetriableError, (err) => {
       if (retryIndex >= DELAYS.length) {
         throw err
@@ -180,10 +191,6 @@ const retryWithBackoff = (fn) => {
         return attempt(retryIndex)
       })
     })
-    .catch(RequestErrors.TransformError, (err) => {
-      // Unroll the error thrown from within the transform
-      throw err.cause
-    })
   }
 
   return attempt(0)
@@ -208,13 +215,8 @@ const tagError = function (err) {
 }
 
 // retry on timeouts, 5xx errors, or any error without a status code
-// do not retry on decryption errors
+// including decryption errors
 const isRetriableError = (err) => {
-  // TransformError means something failed in decryption handling
-  if (err instanceof RequestErrors.TransformError) {
-    return false
-  }
-
   return err instanceof Bluebird.TimeoutError ||
     (err.statusCode >= 500 && err.statusCode < 600) ||
     (err.statusCode == null)
diff --git a/system-tests/__snapshots__/record_spec.js b/system-tests/__snapshots__/record_spec.js
index 54fc701d8b28..d3da7584ac22 100644
--- a/system-tests/__snapshots__/record_spec.js
+++ b/system-tests/__snapshots__/record_spec.js
@@ -2544,7 +2544,7 @@ Available browsers found on your system are:
 - browser3
 `
 
-exports['e2e record api interaction errors sendPreflight [F1] fails on 500 status codes with empty body after retrying 1'] = `
+exports['e2e record api interaction errors sendPreflight [F1] 500 status code errors with empty body fails after retrying 1'] = `
 We encountered an unexpected error communicating with our servers.
 
 StatusCodeError: 500 - "Internal Server Error"
@@ -2562,7 +2562,7 @@ The --ciBuildId flag you passed was: ciBuildId123
 
 `
 
-exports['e2e record api interaction errors sendPreflight [F2] fails on 404 status codes with JSON body without retrying 1'] = `
+exports['e2e record api interaction errors sendPreflight [F2] 404 status code with JSON body fails without retrying 1'] = `
 We could not find a Cypress Cloud project with the projectId: pid123
 
 This projectId came from your cypress-with-project-id.config.js file or an environment variable.
@@ -2577,7 +2577,7 @@ https://on.cypress.io/cloud
 
 `
 
-exports['e2e record api interaction errors sendPreflight [F2] fails on 404 status codes without JSON body without retrying 1'] = `
+exports['e2e record api interaction errors sendPreflight [F2] 404 status codes with empty body fails without retrying 1'] = `
 We could not find a Cypress Cloud project with the projectId: pid123
 
 This projectId came from your cypress-with-project-id.config.js file or an environment variable.
@@ -2721,7 +2721,7 @@ https://on.cypress.io/dashboard/organizations/org-id-1234/billing
 
 `
 
-exports['e2e record api interaction errors sendPreflight [F1] fails on request socket errors after retrying 1'] = `
+exports['e2e record api interaction errors sendPreflight [F1] socket errors fails after retrying 1'] = `
 We encountered an unexpected error communicating with our servers.
 
 RequestError: Error: socket hang up
@@ -2744,6 +2744,30 @@ We encountered an unexpected error communicating with our servers.
 
 DecryptionError: JWE Recipients missing or incorrect type
 
+We will retry 1 more time in X second(s)...
+
+We encountered an unexpected error communicating with our servers.
+
+DecryptionError: JWE Recipients missing or incorrect type
+
+Because you passed the --parallel flag, this run cannot proceed because it requires a valid response from our servers.
+
+The --group flag you passed was: foo
+The --ciBuildId flag you passed was: ciBuildId123
+
+`
+
+exports['e2e record api interaction errors sendPreflight [F1] 500 status code errors with body fails after retrying 1'] = `
+We encountered an unexpected error communicating with our servers.
+
+StatusCodeError: 500 - "Internal Server Error"
+
+We will retry 1 more time in X second(s)...
+
+We encountered an unexpected error communicating with our servers.
+
+StatusCodeError: 500 - "Internal Server Error"
+
 Because you passed the --parallel flag, this run cannot proceed because it requires a valid response from our servers.
 
 The --group flag you passed was: foo
diff --git a/system-tests/test/record_spec.js b/system-tests/test/record_spec.js
index 003f73f4426e..768677d1b7d2 100644
--- a/system-tests/test/record_spec.js
+++ b/system-tests/test/record_spec.js
@@ -1615,7 +1615,7 @@ describe('e2e record', () => {
     })
 
     describe('sendPreflight', () => {
-      describe('[F1]', () => {
+      describe('[F1] socket errors', () => {
         setupStubbedServer(createRoutes({
           sendPreflight: {
             res (req, res) {
@@ -1642,7 +1642,7 @@ describe('e2e record', () => {
         })
       })
 
-      describe('[F1]', () => {
+      describe('[F1] status code errors with empty body', () => {
         setupStubbedServer(createRoutes({
           sendPreflight: {
             res (req, res) {
@@ -1651,7 +1651,36 @@ describe('e2e record', () => {
           },
         }))
 
-        it('fails on 500 status codes with empty body after retrying', function () {
+        it('fails on 500 status codes after retrying', function () {
+          process.env.API_RETRY_INTERVALS = '1000'
+
+          return systemTests.exec(this, {
+            key: 'f858a2bc-b469-4e48-be67-0876339ee7e1',
+            configFile: 'cypress-with-project-id.config.js',
+            spec: 'record_pass*',
+            group: 'foo',
+            tag: 'nightly',
+            record: true,
+            parallel: true,
+            snapshot: true,
+            ciBuildId: 'ciBuildId123',
+            expectedExitCode: 1,
+          })
+        })
+      })
+
+      describe('[F1] status code errors with body', () => {
+        setupStubbedServer(createRoutes({
+          sendPreflight: {
+            res (req, res) {
+              return res
+              .status(500)
+              .json({ message: 'an error message' })
+            },
+          },
+        }))
+
+        it('fails on 500 status codes after retrying', function () {
           process.env.API_RETRY_INTERVALS = '1000'
 
           return systemTests.exec(this, {

From 5d3880772c4698daeae3fbb6ded27d6dc843b21c Mon Sep 17 00:00:00 2001
From: Brian Mann 
Date: Fri, 24 Feb 2023 15:13:46 -0500
Subject: [PATCH 51/52] condensed down to 4 error cases, updated snapshots

---
 packages/server/lib/cloud/api.ts          |  4 ++
 system-tests/__snapshots__/record_spec.js | 16 ++----
 system-tests/test/record_spec.js          | 70 +++++++++++------------
 3 files changed, 44 insertions(+), 46 deletions(-)

diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts
index 0f5b8a8da71c..91ff5a8d99ef 100644
--- a/packages/server/lib/cloud/api.ts
+++ b/packages/server/lib/cloud/api.ts
@@ -217,6 +217,10 @@ const tagError = function (err) {
 // retry on timeouts, 5xx errors, or any error without a status code
 // including decryption errors
 const isRetriableError = (err) => {
+  if (err instanceof DecryptionError) {
+    return false
+  }
+
   return err instanceof Bluebird.TimeoutError ||
     (err.statusCode >= 500 && err.statusCode < 600) ||
     (err.statusCode == null)
diff --git a/system-tests/__snapshots__/record_spec.js b/system-tests/__snapshots__/record_spec.js
index d3da7584ac22..64590f15c8c7 100644
--- a/system-tests/__snapshots__/record_spec.js
+++ b/system-tests/__snapshots__/record_spec.js
@@ -2577,7 +2577,7 @@ https://on.cypress.io/cloud
 
 `
 
-exports['e2e record api interaction errors sendPreflight [F2] 404 status codes with empty body fails without retrying 1'] = `
+exports['e2e record api interaction errors sendPreflight [F2] 404 status code with empty body fails without retrying 1'] = `
 We could not find a Cypress Cloud project with the projectId: pid123
 
 This projectId came from your cypress-with-project-id.config.js file or an environment variable.
@@ -2592,7 +2592,7 @@ https://on.cypress.io/cloud
 
 `
 
-exports['e2e record api interaction errors sendPreflight [F5] fails on OK status codes with invalid unencrypted data without retrying 1'] = `
+exports['e2e record api interaction errors sendPreflight [F3] 201 status code with invalid decryption fails without retrying 1'] = `
 We encountered an unexpected error communicating with our servers.
 
 DecryptionError: JWE Recipients missing or incorrect type
@@ -2604,7 +2604,7 @@ The --ciBuildId flag you passed was: ciBuildId123
 
 `
 
-exports['e2e record api interaction errors sendPreflight [F6] fails on OK status codes with empty body without retrying 1'] = `
+exports['e2e record api interaction errors sendPreflight [F3] 200 status code with empty body fails without retrying 1'] = `
 We encountered an unexpected error communicating with our servers.
 
 DecryptionError: General JWE must be an object
@@ -2616,7 +2616,7 @@ The --ciBuildId flag you passed was: ciBuildId123
 
 `
 
-exports['e2e record api interaction errors sendPreflight [F3] fails on 412 status codes when request is invalid 1'] = `
+exports['e2e record api interaction errors sendPreflight [F4] 412 status code with valid decryption fails without retrying 1'] = `
 Recording this run failed. The request was invalid.
 
 Recording is not working
@@ -2739,13 +2739,7 @@ The --ciBuildId flag you passed was: ciBuildId123
 
 `
 
-exports['e2e record api interaction errors sendPreflight [F4] fails on 422 status codes even when encryption is off 1'] = `
-We encountered an unexpected error communicating with our servers.
-
-DecryptionError: JWE Recipients missing or incorrect type
-
-We will retry 1 more time in X second(s)...
-
+exports['e2e record api interaction errors sendPreflight [F3] 422 status code with invalid decryption fails without retrying 1'] = `
 We encountered an unexpected error communicating with our servers.
 
 DecryptionError: JWE Recipients missing or incorrect type
diff --git a/system-tests/test/record_spec.js b/system-tests/test/record_spec.js
index 768677d1b7d2..d6256c954f4e 100644
--- a/system-tests/test/record_spec.js
+++ b/system-tests/test/record_spec.js
@@ -1624,7 +1624,7 @@ describe('e2e record', () => {
           },
         }))
 
-        it('fails on request socket errors after retrying', function () {
+        it('fails after retrying', function () {
           process.env.API_RETRY_INTERVALS = '1000'
 
           return systemTests.exec(this, {
@@ -1642,7 +1642,7 @@ describe('e2e record', () => {
         })
       })
 
-      describe('[F1] status code errors with empty body', () => {
+      describe('[F1] 500 status code errors with empty body', () => {
         setupStubbedServer(createRoutes({
           sendPreflight: {
             res (req, res) {
@@ -1651,7 +1651,7 @@ describe('e2e record', () => {
           },
         }))
 
-        it('fails on 500 status codes after retrying', function () {
+        it('fails after retrying', function () {
           process.env.API_RETRY_INTERVALS = '1000'
 
           return systemTests.exec(this, {
@@ -1669,7 +1669,7 @@ describe('e2e record', () => {
         })
       })
 
-      describe('[F1] status code errors with body', () => {
+      describe('[F1] 500 status code errors with body', () => {
         setupStubbedServer(createRoutes({
           sendPreflight: {
             res (req, res) {
@@ -1680,7 +1680,7 @@ describe('e2e record', () => {
           },
         }))
 
-        it('fails on 500 status codes after retrying', function () {
+        it('fails after retrying', function () {
           process.env.API_RETRY_INTERVALS = '1000'
 
           return systemTests.exec(this, {
@@ -1698,7 +1698,7 @@ describe('e2e record', () => {
         })
       })
 
-      describe('[F2]', () => {
+      describe('[F2] 404 status code with JSON body', () => {
         setupStubbedServer(createRoutes({
           sendPreflight: {
             res (req, res) {
@@ -1709,7 +1709,7 @@ describe('e2e record', () => {
           },
         }))
 
-        it('fails on 404 status codes with JSON body without retrying', function () {
+        it('fails without retrying', function () {
           process.env.API_RETRY_INTERVALS = '1000'
 
           return systemTests.exec(this, {
@@ -1727,7 +1727,7 @@ describe('e2e record', () => {
         })
       })
 
-      describe('[F2]', () => {
+      describe('[F2] 404 status code with empty body', () => {
         setupStubbedServer(createRoutes({
           sendPreflight: {
             res (req, res) {
@@ -1736,7 +1736,7 @@ describe('e2e record', () => {
           },
         }))
 
-        it('fails on 404 status codes without JSON body without retrying', function () {
+        it('fails without retrying', function () {
           process.env.API_RETRY_INTERVALS = '1000'
 
           return systemTests.exec(this, {
@@ -1754,24 +1754,18 @@ describe('e2e record', () => {
         })
       })
 
-      describe('[F3]', () => {
+      describe('[F3] 422 status code with invalid decryption', () => {
         setupStubbedServer(createRoutes({
           sendPreflight: {
             res: async (req, res) => {
-              return res.status(412).json(await encryptBody(req, res, {
-                message: 'Recording is not working',
-                errors: [
-                  'attempted to send invalid data',
-                ],
-                object: {
-                  projectId: 'cy12345',
-                },
-              }))
+              return res.status(422).json({
+                message: 'something broke',
+              })
             },
           },
         }))
 
-        it('fails on 412 status codes when request is invalid', function () {
+        it('fails without retrying', function () {
           process.env.API_RETRY_INTERVALS = '1000'
 
           return systemTests.exec(this, {
@@ -1789,18 +1783,18 @@ describe('e2e record', () => {
         })
       })
 
-      describe('[F4]', () => {
+      describe('[F3] 201 status code with invalid decryption', () => {
         setupStubbedServer(createRoutes({
           sendPreflight: {
-            res: async (req, res) => {
-              return res.status(422).json({
-                message: 'something broke',
-              })
+            res (req, res) {
+              return res
+              .status(201)
+              .json({ data: 'very encrypted and secure string' })
             },
           },
         }))
 
-        it('fails on 422 status codes even when encryption is off', function () {
+        it('fails without retrying', function () {
           process.env.API_RETRY_INTERVALS = '1000'
 
           return systemTests.exec(this, {
@@ -1818,18 +1812,16 @@ describe('e2e record', () => {
         })
       })
 
-      describe('[F5]', () => {
+      describe('[F3] 200 status code with empty body', () => {
         setupStubbedServer(createRoutes({
           sendPreflight: {
             res (req, res) {
-              return res
-              .status(201)
-              .json({ data: 'very encrypted and secure string' })
+              return res.sendStatus(200)
             },
           },
         }))
 
-        it('fails on OK status codes with invalid unencrypted data without retrying', function () {
+        it('fails without retrying', function () {
           process.env.API_RETRY_INTERVALS = '1000'
 
           return systemTests.exec(this, {
@@ -1847,16 +1839,24 @@ describe('e2e record', () => {
         })
       })
 
-      describe('[F6]', () => {
+      describe('[F4] 412 status code with valid decryption', () => {
         setupStubbedServer(createRoutes({
           sendPreflight: {
-            res (req, res) {
-              return res.sendStatus(200)
+            res: async (req, res) => {
+              return res.status(412).json(await encryptBody(req, res, {
+                message: 'Recording is not working',
+                errors: [
+                  'attempted to send invalid data',
+                ],
+                object: {
+                  projectId: 'cy12345',
+                },
+              }))
             },
           },
         }))
 
-        it('fails on OK status codes with empty body without retrying', function () {
+        it('fails without retrying', function () {
           process.env.API_RETRY_INTERVALS = '1000'
 
           return systemTests.exec(this, {

From 45c8a33f1c3e8d62ed419181a61a63e0b98d3543 Mon Sep 17 00:00:00 2001
From: Brian Mann 
Date: Fri, 24 Feb 2023 16:06:07 -0500
Subject: [PATCH 52/52] added tests for failing after preflight, but valid
 decryption, always fail on 412 preflight

- updated [W1] warning test case
- updated unit test cases names
---
 packages/server/lib/cloud/api.ts            | 10 ++-
 packages/server/test/unit/cloud/api_spec.js | 76 ++++++++++++++++++---
 system-tests/__snapshots__/record_spec.js   | 15 +++-
 system-tests/test/record_spec.js            | 53 +++++++++++++-
 4 files changed, 142 insertions(+), 12 deletions(-)

diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts
index 91ff5a8d99ef..0afda703c4b4 100644
--- a/packages/server/lib/cloud/api.ts
+++ b/packages/server/lib/cloud/api.ts
@@ -477,7 +477,7 @@ module.exports = {
       const preflightBaseProxy = apiUrl.replace('api', 'api-proxy')
 
       const envInformation = await getEnvInformationForProjectRoot(projectRoot, process.pid.toString())
-      const makeReq = async ({ baseUrl, agent }) => {
+      const makeReq = ({ baseUrl, agent }) => {
         return rp.post({
           url: `${baseUrl}preflight`,
           body: {
@@ -496,11 +496,19 @@ module.exports = {
           encrypt: 'always',
           agent,
         })
+        .catch(RequestErrors.TransformError, (err) => {
+          // Unroll the error thrown from within the transform
+          throw err.cause
+        })
       }
 
       const postReqs = async () => {
         return makeReq({ baseUrl: preflightBaseProxy, agent: null })
         .catch((err) => {
+          if (err.statusCode === 412) {
+            throw err
+          }
+
           return makeReq({ baseUrl: apiUrl, agent })
         })
       }
diff --git a/packages/server/test/unit/cloud/api_spec.js b/packages/server/test/unit/cloud/api_spec.js
index 23aa75edb489..e9bdfbf0cb17 100644
--- a/packages/server/test/unit/cloud/api_spec.js
+++ b/packages/server/test/unit/cloud/api_spec.js
@@ -326,7 +326,7 @@ describe('lib/cloud/api', () => {
     })
 
     describe('errors', () => {
-      it('handles timeout', () => {
+      it('[F1] POST /preflight TimeoutError', () => {
         preflightNock(API_BASEURL)
         .times(2)
         .delayConnection(5000)
@@ -412,12 +412,30 @@ describe('lib/cloud/api', () => {
         })
       })
 
-      // TODO: finish implementing this test
-      it.skip('[F3] POST /preflight statusCode = 422/412', () => {
+      it('[F3] POST /preflight statusCode = 422 but decrypt error', () => {
+        const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
+        .reply(422, { data: 'very encrypted and secure string' })
+
+        const scopeApi = preflightNock(API_PROD_BASEURL)
+        .reply(422, { data: 'very encrypted and secure string' })
 
+        return prodApi.sendPreflight({ projectId: 'abc123' })
+        .then(() => {
+          throw new Error('should have thrown here')
+        })
+        .catch((err) => {
+          scopeProxy.done()
+          scopeApi.done()
+
+          expect(err).not.to.have.property('statusCode')
+          expect(err).to.contain({
+            name: 'DecryptionError',
+            message: 'JWE Recipients missing or incorrect type',
+          })
+        })
       })
 
-      it('[F5] POST /preflight statusCode OK but decrypt error', () => {
+      it('[F3] POST /preflight statusCode = 200 but decrypt error', () => {
         const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
         .reply(200, { data: 'very encrypted and secure string' })
 
@@ -434,13 +452,13 @@ describe('lib/cloud/api', () => {
 
           expect(err).not.to.have.property('statusCode')
           expect(err).to.contain({
-            name: 'TransformError',
-            message: 'DecryptionError: General JWE must be an object',
+            name: 'DecryptionError',
+            message: 'General JWE must be an object',
           })
         })
       })
 
-      it('[F6] POST /preflight statusCode OK but no body', () => {
+      it('[F3] POST /preflight statusCode = 201 but no body', () => {
         const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
         .reply(200)
 
@@ -457,8 +475,48 @@ describe('lib/cloud/api', () => {
 
           expect(err).not.to.have.property('statusCode')
           expect(err).to.contain({
-            name: 'TransformError',
-            message: 'DecryptionError: General JWE must be an object',
+            name: 'DecryptionError',
+            message: 'General JWE must be an object',
+          })
+        })
+      })
+
+      it('[F4] POST /preflight statusCode = 412 valid decryption', () => {
+        const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
+        .reply(412, decryptReqBodyAndRespond({
+          reqBody: {
+            envUrl: 'https://some.server.com',
+            dependencies: {},
+            errors: [],
+            apiUrl: 'https://api.cypress.io/',
+            projectId: 'abc123',
+          },
+          resBody: {
+            message: 'Recording is not working',
+            errors: [
+              'attempted to send invalid data',
+            ],
+            object: {
+              projectId: 'cy12345',
+            },
+          },
+        }))
+
+        const scopeApi = preflightNock(API_PROD_BASEURL)
+        .reply(200)
+
+        return prodApi.sendPreflight({ projectId: 'abc123' })
+        .then(() => {
+          throw new Error('should have thrown here')
+        })
+        .catch((err) => {
+          scopeProxy.done()
+          expect(scopeApi.isDone()).to.be.false
+
+          expect(err).to.contain({
+            name: 'StatusCodeError',
+            message: '412 - {"message":"Recording is not working","errors":["attempted to send invalid data"],"object":{"projectId":"cy12345"}}',
+            statusCode: 412,
           })
         })
       })
diff --git a/system-tests/__snapshots__/record_spec.js b/system-tests/__snapshots__/record_spec.js
index 64590f15c8c7..b2ee918e72a1 100644
--- a/system-tests/__snapshots__/record_spec.js
+++ b/system-tests/__snapshots__/record_spec.js
@@ -2635,7 +2635,7 @@ Request Sent:
 
 `
 
-exports['e2e record api interaction errors sendPreflight preflight failure: warning message renders preflight warning messages prior to run warnings 1'] = `
+exports['e2e record api interaction errors sendPreflight [W1] warning message renders preflight warning messages prior to run warnings 1'] = `
 Warning from Cypress Cloud: 
 
 ----------------------------------------------------------------------
@@ -2768,3 +2768,16 @@ The --group flag you passed was: foo
 The --ciBuildId flag you passed was: ciBuildId123
 
 `
+
+exports['e2e record api interaction errors sendPreflight [F5] 422 status code with valid decryption on createRun errors and exits when group name is in use 1'] = `
+You passed the --group flag, but this group name has already been used for this run.
+
+The existing run is: https://cloud.cypress.io/runs/12345
+
+The --group flag you passed was: e2e-tests
+
+If you are trying to parallelize this run, then also pass the --parallel flag, else pass a different group name.
+
+https://on.cypress.io/run-group-name-not-unique
+
+`
diff --git a/system-tests/test/record_spec.js b/system-tests/test/record_spec.js
index d6256c954f4e..c0139be58768 100644
--- a/system-tests/test/record_spec.js
+++ b/system-tests/test/record_spec.js
@@ -1874,7 +1874,58 @@ describe('e2e record', () => {
         })
       })
 
-      describe('preflight failure: warning message', () => {
+      describe('[F5] 422 status code with valid decryption on createRun', async () => {
+        const mockServer = setupStubbedServer(createRoutes({
+          sendPreflight: {
+            res: async (req, res) => {
+              return res.json(await encryptBody(req, res, {
+                encrypt: true,
+                apiUrl: req.body.apiUrl,
+              }))
+            },
+          },
+          postRun: {
+            res: async (req, res) => {
+              mockServer.setSpecs(req)
+
+              return res
+              .set({ 'x-cypress-encrypted': true })
+              .status(422)
+              .json(await encryptBody(req, res, {
+                code: 'RUN_GROUP_NAME_NOT_UNIQUE',
+                message: 'Run group name cannot be used again without passing the parallel flag.',
+                payload: {
+                  runUrl: 'https://cloud.cypress.io/runs/12345',
+                },
+              }))
+            },
+          },
+        }))
+
+        // the other 422 tests for this are in integration/cypress_spec
+        it('errors and exits when group name is in use', function () {
+          process.env.CIRCLECI = '1'
+
+          return systemTests.exec(this, {
+            key: 'f858a2bc-b469-4e48-be67-0876339ee7e1',
+            configFile: 'cypress-with-project-id.config.js',
+            spec: 'record_pass*',
+            group: 'e2e-tests',
+            record: true,
+            snapshot: true,
+            expectedExitCode: 1,
+          })
+          .then(() => {
+            const urls = getRequestUrls()
+
+            expect(urls).to.deep.eq([
+              'POST /runs',
+            ])
+          })
+        })
+      })
+
+      describe('[W1] warning message', () => {
         const mockServer = setupStubbedServer(createRoutes({
           sendPreflight: {
             res: async (req, res) => {