From b7ea1310eb2e20e609b38caeac5216288fea882b Mon Sep 17 00:00:00 2001 From: Mabel Amaya Date: Thu, 5 Feb 2026 17:53:35 -0500 Subject: [PATCH 1/8] chore: send debugData to studio server --- packages/data-context/src/data/ProjectConfigIpc.ts | 5 +---- packages/data-context/src/data/ProjectConfigManager.ts | 3 ++- packages/server/lib/cloud/studio/StudioLifecycleManager.ts | 6 ++++-- packages/server/lib/cloud/studio/studio.ts | 5 ++++- packages/types/src/debug.ts | 7 +++++++ packages/types/src/index.ts | 2 ++ packages/types/src/protocol.ts | 5 ++--- packages/types/src/studio/studio-server-types.ts | 2 ++ 8 files changed, 24 insertions(+), 11 deletions(-) create mode 100644 packages/types/src/debug.ts diff --git a/packages/data-context/src/data/ProjectConfigIpc.ts b/packages/data-context/src/data/ProjectConfigIpc.ts index 8a85aa9be5..cb2f343ca6 100644 --- a/packages/data-context/src/data/ProjectConfigIpc.ts +++ b/packages/data-context/src/data/ProjectConfigIpc.ts @@ -1,5 +1,6 @@ /* eslint-disable no-dupe-class-members */ import { CypressError, getError } from '@packages/errors' +import type { DebugData } from '@packages/types/src/debug' import type { FullConfig, TestingType } from '@packages/types' import { ChildProcess, fork, ForkOptions, spawn } from 'child_process' import EventEmitter from 'events' @@ -50,10 +51,6 @@ interface SerializedLoadConfigReply { requires: string[] } -export interface DebugData { - filePreprocessorHandlerText?: string -} - /** * The ProjectConfigIpc is an EventEmitter wrapping the childProcess, * adding a "send" method for sending events from the parent process into the childProcess, diff --git a/packages/data-context/src/data/ProjectConfigManager.ts b/packages/data-context/src/data/ProjectConfigManager.ts index 78c617b4a9..8e57e7a774 100644 --- a/packages/data-context/src/data/ProjectConfigManager.ts +++ b/packages/data-context/src/data/ProjectConfigManager.ts @@ -1,6 +1,7 @@ import { CypressError, getError } from '@packages/errors' -import { DebugData, PluginIpcHandler, LoadConfigReply, ProjectConfigIpc, SetupNodeEventsReply } from './ProjectConfigIpc' +import { PluginIpcHandler, LoadConfigReply, ProjectConfigIpc, SetupNodeEventsReply } from './ProjectConfigIpc' import assert from 'assert' +import type { DebugData } from '@packages/types/src/debug' import type { AllModeOptions, FullConfig, TestingType } from '@packages/types' import debugLib from 'debug' import path from 'path' diff --git a/packages/server/lib/cloud/studio/StudioLifecycleManager.ts b/packages/server/lib/cloud/studio/StudioLifecycleManager.ts index 743e141755..cba14b2b96 100644 --- a/packages/server/lib/cloud/studio/StudioLifecycleManager.ts +++ b/packages/server/lib/cloud/studio/StudioLifecycleManager.ts @@ -25,6 +25,7 @@ import { INITIALIZATION_TELEMETRY_GROUP_NAMES } from './telemetry/constants/init import crypto from 'crypto' import { logError } from '@packages/stderr-filtering' import { isNonRetriableCertErrorCode } from '../network/non_retriable_cert_error_codes' +import { DebugData } from '@packages/types/src/debug' const debug = Debug('cypress:server:studio-lifecycle-manager') const routes = require('../routes') @@ -179,7 +180,7 @@ export class StudioLifecycleManager { }: { cloudDataSource: CloudDataSource cfg: Cfg - debugData: any + debugData?: DebugData getProjectOptions: Required['getProjectOptions'] }): Promise { let studioPath: string @@ -277,6 +278,7 @@ export class StudioLifecycleManager { }, manifest, getProjectOptions, + debugData, }) telemetryManager.mark(BUNDLE_LIFECYCLE_MARK_NAMES.STUDIO_MANAGER_SETUP_END) @@ -349,7 +351,7 @@ export class StudioLifecycleManager { }: { cloudDataSource: CloudDataSource cfg: Cfg - debugData: any + debugData?: DebugData getProjectOptions: Required['getProjectOptions'] }) { // Don't setup a watcher if the studio bundle is NOT local diff --git a/packages/server/lib/cloud/studio/studio.ts b/packages/server/lib/cloud/studio/studio.ts index 8931d35223..b237e6079c 100644 --- a/packages/server/lib/cloud/studio/studio.ts +++ b/packages/server/lib/cloud/studio/studio.ts @@ -6,6 +6,7 @@ import path from 'path' import crypto, { BinaryLike } from 'crypto' import { StudioElectron } from './StudioElectron' import exception from '../exception' +import type { DebugData } from '@packages/types/src/debug' interface StudioServer { default: StudioServerDefaultShape } @@ -16,6 +17,7 @@ interface SetupOptions { cloudApi: StudioCloudApi manifest: Record getProjectOptions: StudioServerOptions['getProjectOptions'] + debugData?: DebugData } const debug = Debug('cypress:server:studio') @@ -26,7 +28,7 @@ export class StudioManager implements StudioManagerShape { private _studioServer: StudioServerShape | undefined private _studioElectron: StudioElectron | undefined - async setup ({ script, studioPath, studioHash, cloudApi, manifest, getProjectOptions }: SetupOptions): Promise { + async setup ({ script, studioPath, studioHash, cloudApi, manifest, getProjectOptions, debugData }: SetupOptions): Promise { const { createStudioServer } = requireScript(script).default this._studioServer = await createStudioServer({ @@ -47,6 +49,7 @@ export class StudioManager implements StudioManagerShape { return actualHash === expectedHash }, getProjectOptions, + debugData, }) this.status = 'ENABLED' diff --git a/packages/types/src/debug.ts b/packages/types/src/debug.ts new file mode 100644 index 0000000000..a3e50f9ef2 --- /dev/null +++ b/packages/types/src/debug.ts @@ -0,0 +1,7 @@ +/** + * Debug data shared by config loading, protocol manager, and studio. + * Used to surface diagnostic information (e.g. file preprocessor handler text). + */ +export interface DebugData { + filePreprocessorHandlerText?: string +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index c3ea8274b2..54d5852c14 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -32,6 +32,8 @@ export * from './reporter' export * from './server' +export * from './debug' + export * from './util' export * from './types' diff --git a/packages/types/src/protocol.ts b/packages/types/src/protocol.ts index 8aabd1e824..a0a8e18f9b 100644 --- a/packages/types/src/protocol.ts +++ b/packages/types/src/protocol.ts @@ -2,6 +2,7 @@ import type Database from 'better-sqlite3' import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping' import type { IncomingHttpHeaders } from 'http' import type { Readable } from 'stream' +import type { DebugData } from './debug' import type { ProxyTimings } from './proxy' import type { FoundSpec } from './spec' @@ -105,9 +106,7 @@ export type ProtocolManagerOptions = { } projectConfig: ProjectConfig mountVersion?: number - debugData?: { - filePreprocessorHandlerText?: string - } + debugData?: DebugData mode?: 'record' | 'studio' } diff --git a/packages/types/src/studio/studio-server-types.ts b/packages/types/src/studio/studio-server-types.ts index e9c0a2badb..b2674179a5 100644 --- a/packages/types/src/studio/studio-server-types.ts +++ b/packages/types/src/studio/studio-server-types.ts @@ -9,6 +9,7 @@ import type { Router } from 'express' import type { AxiosInstance } from 'axios' import type { Socket } from 'socket.io' import type { BinaryLike } from 'crypto' +import type { DebugData } from '../debug' export const StudioMetricsTypes = { STUDIO_STARTED: 'studio:started', @@ -100,6 +101,7 @@ export interface StudioServerOptions { manifest?: Record verifyHash: (contents: BinaryLike, expectedHash: string) => boolean studioElectron?: StudioElectronApi + debugData?: DebugData } export interface StudioAIInitializeOptions { From 3047e85c766b66254230eadc83cb63225d3aff4f Mon Sep 17 00:00:00 2001 From: Mabel Amaya Date: Fri, 6 Feb 2026 09:27:05 -0500 Subject: [PATCH 2/8] add tests for debugdata --- .../studio/StudioLifecycleManager_spec.ts | 107 ++++++++++++++++++ .../test/unit/cloud/studio/studio_spec.ts | 83 ++++++++++++++ 2 files changed, 190 insertions(+) diff --git a/packages/server/test/unit/cloud/studio/StudioLifecycleManager_spec.ts b/packages/server/test/unit/cloud/studio/StudioLifecycleManager_spec.ts index 6394b357a5..4090d32527 100644 --- a/packages/server/test/unit/cloud/studio/StudioLifecycleManager_spec.ts +++ b/packages/server/test/unit/cloud/studio/StudioLifecycleManager_spec.ts @@ -234,6 +234,7 @@ describe('StudioLifecycleManager', () => { asyncRetry, }, manifest: mockManifest, + debugData: {}, }) expect(postStudioSessionStub).to.be.calledWith({ @@ -332,6 +333,7 @@ describe('StudioLifecycleManager', () => { asyncRetry, }, manifest: {}, + debugData: {}, }) expect(postStudioSessionStub).to.be.calledWith({ @@ -392,6 +394,111 @@ describe('StudioLifecycleManager', () => { expect(await mockStudioManagerPromise).to.equal(updatedStudioManager) }) + it('initializes the studio manager and sends debug data to the studio server', async () => { + const debugData = { + filePreprocessorHandlerText: 'handler text', + } + + studioManagerSetupStub.callsFake((args) => { + mockStudioManager.status = 'ENABLED' + + return Promise.resolve() + }) + + studioLifecycleManager.initializeStudioManager({ + cloudDataSource: mockCloudDataSource, + ctx: mockCtx, + cfg: mockCfg, + debugData, + }) + + const studioReadyPromise = new Promise((resolve) => { + studioLifecycleManager?.registerStudioReadyListener((studioManager) => { + resolve(studioManager) + }) + }) + + const mockManifest = { + 'server/index.js': 'e1ed3dc8ba9eb8ece23914004b99ad97bba37e80a25d8b47c009e1e4948a6159', + } + + ensureStudioBundleStub.resolves(mockManifest) + + await studioReadyPromise + + expect(mockCtx.update).to.be.calledOnce + expect(ensureStudioBundleStub).to.be.calledWith({ + studioPath: path.join(os.tmpdir(), 'cypress', 'studio', 'abc'), + studioUrl: 'https://cloud.cypress.io/studio/bundle/abc.tgz', + projectId: 'abc123', + }) + + expect(studioManagerSetupStub).to.be.calledWith({ + script: 'console.log("studio script")', + studioPath: path.join(os.tmpdir(), 'cypress', 'studio', 'abc'), + studioHash: 'abc', + getProjectOptions: sinon.match.func, + cloudApi: { + cloudUrl: 'https://cloud.cypress.io', + cloudHeaders: { 'Authorization': 'Bearer test-token' }, + CloudRequest, + isRetryableError, + asyncRetry, + }, + manifest: mockManifest, + debugData, + }) + + expect(postStudioSessionStub).to.be.calledWith({ + projectId: 'abc123', + }) + + expect(readFileStub).to.be.calledWith(path.join(os.tmpdir(), 'cypress', 'studio', 'abc', 'server', 'index.js'), 'utf8') + + expect(getCaptureProtocolScriptStub).to.be.calledWith('https://cloud.cypress.io/capture-protocol/script/def.js') + expect(prepareProtocolStub).to.be.calledWith('console.log("hello")', { + runId: 'studio', + projectId: 'abc123', + testingType: 'e2e', + cloudApi: { + url: 'http://localhost:1234/', + retryWithBackoff: api.retryWithBackoff, + requestPromise: api.rp, + }, + projectConfig: { + devServerPublicPathRoute: '/__cypress/src', + namespace: '__cypress', + port: 8888, + proxyUrl: 'http://localhost:8888', + }, + mountVersion: 2, + debugData, + mode: 'studio', + }) + + expect(initializeTelemetryReporterStub).to.be.calledWith({ + projectSlug: 'abc123', + cloudDataSource: mockCloudDataSource, + }) + + expect(markStub).to.be.calledWith(BUNDLE_LIFECYCLE_MARK_NAMES.BUNDLE_LIFECYCLE_START) + expect(markStub).to.be.calledWith(BUNDLE_LIFECYCLE_MARK_NAMES.BUNDLE_LIFECYCLE_END) + expect(markStub).to.be.calledWith(BUNDLE_LIFECYCLE_MARK_NAMES.POST_STUDIO_SESSION_START) + expect(markStub).to.be.calledWith(BUNDLE_LIFECYCLE_MARK_NAMES.POST_STUDIO_SESSION_END) + expect(markStub).to.be.calledWith(BUNDLE_LIFECYCLE_MARK_NAMES.ENSURE_STUDIO_BUNDLE_START) + expect(markStub).to.be.calledWith(BUNDLE_LIFECYCLE_MARK_NAMES.ENSURE_STUDIO_BUNDLE_END) + expect(markStub).to.be.calledWith(BUNDLE_LIFECYCLE_MARK_NAMES.STUDIO_MANAGER_SETUP_START) + expect(markStub).to.be.calledWith(BUNDLE_LIFECYCLE_MARK_NAMES.STUDIO_MANAGER_SETUP_END) + expect(markStub).to.be.calledWith(BUNDLE_LIFECYCLE_MARK_NAMES.STUDIO_PROTOCOL_GET_START) + expect(markStub).to.be.calledWith(BUNDLE_LIFECYCLE_MARK_NAMES.STUDIO_PROTOCOL_GET_END) + expect(markStub).to.be.calledWith(BUNDLE_LIFECYCLE_MARK_NAMES.STUDIO_PROTOCOL_PREPARE_START) + expect(markStub).to.be.calledWith(BUNDLE_LIFECYCLE_MARK_NAMES.STUDIO_PROTOCOL_PREPARE_END) + + expect(reportTelemetryStub).to.be.calledWith(BUNDLE_LIFECYCLE_TELEMETRY_GROUP_NAMES.COMPLETE_BUNDLE_LIFECYCLE, { + success: true, + }) + }) + it('throws an error when the studio server script is not found in the manifest', async () => { studioManagerSetupStub.callsFake((args) => { mockStudioManager.status = 'ENABLED' diff --git a/packages/server/test/unit/cloud/studio/studio_spec.ts b/packages/server/test/unit/cloud/studio/studio_spec.ts index 221cdc2c5c..f87e66b11e 100644 --- a/packages/server/test/unit/cloud/studio/studio_spec.ts +++ b/packages/server/test/unit/cloud/studio/studio_spec.ts @@ -65,6 +65,89 @@ describe('lib/cloud/studio', () => { sinon.restore() }) + describe('setup', () => { + it('passes debugData to createStudioServer when provided', async () => { + const createStudioServerStub = sinon.stub().resolves({ + initializeRoutes: sinon.stub(), + canAccessStudioAI: sinon.stub().resolves(true), + initializeStudioAI: sinon.stub().resolves(), + reportError: sinon.stub(), + destroy: sinon.stub().resolves(), + addSocketListeners: sinon.stub(), + captureStudioEvent: sinon.stub().resolves(), + updateSessionId: sinon.stub(), + connectToBrowser: sinon.stub(), + }) + + const StubbedStudioManager = (proxyquire('../lib/cloud/studio/studio', { + '../api/studio/report_studio_error': { reportStudioError: sinon.stub() }, + './StudioElectron': { StudioElectron: class {} }, + '../require_script': { + requireScript: () => ({ + default: { createStudioServer: createStudioServerStub }, + }), + }, + }) as typeof import('@packages/server/lib/cloud/studio/studio')).StudioManager + + const manager = new StubbedStudioManager() + const debugData = { filePreprocessorHandlerText: 'handler text' } + + await manager.setup({ + script: 'script', + studioPath: 'path', + studioHash: 'abcdefg', + getProjectOptions: sinon.stub().resolves({ projectSlug: '1234' }), + cloudApi: {} as any, + manifest: { 'server/index.js': 'abcdefg' }, + debugData, + }) + + expect(createStudioServerStub).to.have.been.calledOnce + expect(createStudioServerStub.firstCall.args[0].debugData).to.deep.equal(debugData) + }) + + it('passes undefined debugData to createStudioServer when not provided', async () => { + const createStudioServerStub = sinon.stub().resolves({ + initializeRoutes: sinon.stub(), + canAccessStudioAI: sinon.stub().resolves(true), + initializeStudioAI: sinon.stub().resolves(), + reportError: sinon.stub(), + destroy: sinon.stub().resolves(), + addSocketListeners: sinon.stub(), + captureStudioEvent: sinon.stub().resolves(), + updateSessionId: sinon.stub(), + connectToBrowser: sinon.stub(), + }) + + const StubbedStudioManager = (proxyquire('../lib/cloud/studio/studio', { + '../api/studio/report_studio_error': { reportStudioError: sinon.stub() }, + './StudioElectron': { StudioElectron: class {} }, + '../require_script': { + requireScript: () => ({ + default: { createStudioServer: createStudioServerStub }, + }), + }, + }) as typeof import('@packages/server/lib/cloud/studio/studio')).StudioManager + + const manager = new StubbedStudioManager() + + await manager.setup({ + script: 'script', + studioPath: 'path', + studioHash: 'abcdefg', + getProjectOptions: sinon.stub().resolves({ projectSlug: '1234' }), + cloudApi: {} as any, + manifest: { 'server/index.js': 'abcdefg' }, + }) + + expect(createStudioServerStub).to.have.been.calledOnce + const options = createStudioServerStub.firstCall.args[0] + + expect(options).to.have.property('debugData') + expect(options.debugData).to.be.undefined + }) + }) + describe('synchronous method invocation', () => { it('reports an error when a synchronous method fails', () => { const error = new Error('foo') From ab80ae487691e67128efa68622b0253918ba4c4a Mon Sep 17 00:00:00 2001 From: Mabel Amaya Date: Fri, 6 Feb 2026 09:39:03 -0500 Subject: [PATCH 3/8] use import type --- packages/server/lib/cloud/studio/StudioLifecycleManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/lib/cloud/studio/StudioLifecycleManager.ts b/packages/server/lib/cloud/studio/StudioLifecycleManager.ts index cba14b2b96..c10729f10e 100644 --- a/packages/server/lib/cloud/studio/StudioLifecycleManager.ts +++ b/packages/server/lib/cloud/studio/StudioLifecycleManager.ts @@ -25,7 +25,7 @@ import { INITIALIZATION_TELEMETRY_GROUP_NAMES } from './telemetry/constants/init import crypto from 'crypto' import { logError } from '@packages/stderr-filtering' import { isNonRetriableCertErrorCode } from '../network/non_retriable_cert_error_codes' -import { DebugData } from '@packages/types/src/debug' +import type { DebugData } from '@packages/types/src/debug' const debug = Debug('cypress:server:studio-lifecycle-manager') const routes = require('../routes') From 0caf87c3b5e2ee1219106e063ef761bf33a9d281 Mon Sep 17 00:00:00 2001 From: Mabel Amaya Date: Fri, 6 Feb 2026 16:25:05 -0500 Subject: [PATCH 4/8] remove new type that is not allowed --- packages/types/src/studio/studio-server-types.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/types/src/studio/studio-server-types.ts b/packages/types/src/studio/studio-server-types.ts index b2674179a5..e9c0a2badb 100644 --- a/packages/types/src/studio/studio-server-types.ts +++ b/packages/types/src/studio/studio-server-types.ts @@ -9,7 +9,6 @@ import type { Router } from 'express' import type { AxiosInstance } from 'axios' import type { Socket } from 'socket.io' import type { BinaryLike } from 'crypto' -import type { DebugData } from '../debug' export const StudioMetricsTypes = { STUDIO_STARTED: 'studio:started', @@ -101,7 +100,6 @@ export interface StudioServerOptions { manifest?: Record verifyHash: (contents: BinaryLike, expectedHash: string) => boolean studioElectron?: StudioElectronApi - debugData?: DebugData } export interface StudioAIInitializeOptions { From d0e87dddc5442b12c19b9d222211cc91c9bfca7c Mon Sep 17 00:00:00 2001 From: Mabel Amaya Date: Fri, 6 Feb 2026 16:25:26 -0500 Subject: [PATCH 5/8] remove new test and use existing one --- .../studio/StudioLifecycleManager_spec.ts | 115 +----------------- 1 file changed, 6 insertions(+), 109 deletions(-) diff --git a/packages/server/test/unit/cloud/studio/StudioLifecycleManager_spec.ts b/packages/server/test/unit/cloud/studio/StudioLifecycleManager_spec.ts index 4090d32527..607a3c938d 100644 --- a/packages/server/test/unit/cloud/studio/StudioLifecycleManager_spec.ts +++ b/packages/server/test/unit/cloud/studio/StudioLifecycleManager_spec.ts @@ -21,6 +21,8 @@ const api = require('../../../../lib/cloud/api').default // Helper to wait for next tick in event loop const nextTick = () => new Promise((resolve) => process.nextTick(resolve)) +const debugData = { filePreprocessorHandlerText: 'handler text' } + describe('StudioLifecycleManager', () => { let studioLifecycleManager: StudioLifecycleManager let mockStudioManager: StudioManager @@ -197,7 +199,7 @@ describe('StudioLifecycleManager', () => { cloudDataSource: mockCloudDataSource, ctx: mockCtx, cfg: mockCfg, - debugData: {}, + debugData, }) const studioReadyPromise = new Promise((resolve) => { @@ -234,7 +236,7 @@ describe('StudioLifecycleManager', () => { asyncRetry, }, manifest: mockManifest, - debugData: {}, + debugData, }) expect(postStudioSessionStub).to.be.calledWith({ @@ -260,7 +262,7 @@ describe('StudioLifecycleManager', () => { proxyUrl: 'http://localhost:8888', }, mountVersion: 2, - debugData: {}, + debugData, mode: 'studio', }) @@ -352,7 +354,7 @@ describe('StudioLifecycleManager', () => { retryWithBackoff: api.retryWithBackoff, requestPromise: api.rp, }, - projectConfig: { + projectConfig: { devServerPublicPathRoute: '/__cypress/src', namespace: '__cypress', port: 8888, @@ -394,111 +396,6 @@ describe('StudioLifecycleManager', () => { expect(await mockStudioManagerPromise).to.equal(updatedStudioManager) }) - it('initializes the studio manager and sends debug data to the studio server', async () => { - const debugData = { - filePreprocessorHandlerText: 'handler text', - } - - studioManagerSetupStub.callsFake((args) => { - mockStudioManager.status = 'ENABLED' - - return Promise.resolve() - }) - - studioLifecycleManager.initializeStudioManager({ - cloudDataSource: mockCloudDataSource, - ctx: mockCtx, - cfg: mockCfg, - debugData, - }) - - const studioReadyPromise = new Promise((resolve) => { - studioLifecycleManager?.registerStudioReadyListener((studioManager) => { - resolve(studioManager) - }) - }) - - const mockManifest = { - 'server/index.js': 'e1ed3dc8ba9eb8ece23914004b99ad97bba37e80a25d8b47c009e1e4948a6159', - } - - ensureStudioBundleStub.resolves(mockManifest) - - await studioReadyPromise - - expect(mockCtx.update).to.be.calledOnce - expect(ensureStudioBundleStub).to.be.calledWith({ - studioPath: path.join(os.tmpdir(), 'cypress', 'studio', 'abc'), - studioUrl: 'https://cloud.cypress.io/studio/bundle/abc.tgz', - projectId: 'abc123', - }) - - expect(studioManagerSetupStub).to.be.calledWith({ - script: 'console.log("studio script")', - studioPath: path.join(os.tmpdir(), 'cypress', 'studio', 'abc'), - studioHash: 'abc', - getProjectOptions: sinon.match.func, - cloudApi: { - cloudUrl: 'https://cloud.cypress.io', - cloudHeaders: { 'Authorization': 'Bearer test-token' }, - CloudRequest, - isRetryableError, - asyncRetry, - }, - manifest: mockManifest, - debugData, - }) - - expect(postStudioSessionStub).to.be.calledWith({ - projectId: 'abc123', - }) - - expect(readFileStub).to.be.calledWith(path.join(os.tmpdir(), 'cypress', 'studio', 'abc', 'server', 'index.js'), 'utf8') - - expect(getCaptureProtocolScriptStub).to.be.calledWith('https://cloud.cypress.io/capture-protocol/script/def.js') - expect(prepareProtocolStub).to.be.calledWith('console.log("hello")', { - runId: 'studio', - projectId: 'abc123', - testingType: 'e2e', - cloudApi: { - url: 'http://localhost:1234/', - retryWithBackoff: api.retryWithBackoff, - requestPromise: api.rp, - }, - projectConfig: { - devServerPublicPathRoute: '/__cypress/src', - namespace: '__cypress', - port: 8888, - proxyUrl: 'http://localhost:8888', - }, - mountVersion: 2, - debugData, - mode: 'studio', - }) - - expect(initializeTelemetryReporterStub).to.be.calledWith({ - projectSlug: 'abc123', - cloudDataSource: mockCloudDataSource, - }) - - expect(markStub).to.be.calledWith(BUNDLE_LIFECYCLE_MARK_NAMES.BUNDLE_LIFECYCLE_START) - expect(markStub).to.be.calledWith(BUNDLE_LIFECYCLE_MARK_NAMES.BUNDLE_LIFECYCLE_END) - expect(markStub).to.be.calledWith(BUNDLE_LIFECYCLE_MARK_NAMES.POST_STUDIO_SESSION_START) - expect(markStub).to.be.calledWith(BUNDLE_LIFECYCLE_MARK_NAMES.POST_STUDIO_SESSION_END) - expect(markStub).to.be.calledWith(BUNDLE_LIFECYCLE_MARK_NAMES.ENSURE_STUDIO_BUNDLE_START) - expect(markStub).to.be.calledWith(BUNDLE_LIFECYCLE_MARK_NAMES.ENSURE_STUDIO_BUNDLE_END) - expect(markStub).to.be.calledWith(BUNDLE_LIFECYCLE_MARK_NAMES.STUDIO_MANAGER_SETUP_START) - expect(markStub).to.be.calledWith(BUNDLE_LIFECYCLE_MARK_NAMES.STUDIO_MANAGER_SETUP_END) - expect(markStub).to.be.calledWith(BUNDLE_LIFECYCLE_MARK_NAMES.STUDIO_PROTOCOL_GET_START) - expect(markStub).to.be.calledWith(BUNDLE_LIFECYCLE_MARK_NAMES.STUDIO_PROTOCOL_GET_END) - expect(markStub).to.be.calledWith(BUNDLE_LIFECYCLE_MARK_NAMES.STUDIO_PROTOCOL_PREPARE_START) - expect(markStub).to.be.calledWith(BUNDLE_LIFECYCLE_MARK_NAMES.STUDIO_PROTOCOL_PREPARE_END) - - expect(reportTelemetryStub).to.be.calledWith(BUNDLE_LIFECYCLE_TELEMETRY_GROUP_NAMES.COMPLETE_BUNDLE_LIFECYCLE, { - success: true, - }) - }) - it('throws an error when the studio server script is not found in the manifest', async () => { studioManagerSetupStub.callsFake((args) => { mockStudioManager.status = 'ENABLED' From a130ed250ebf58fdde70613a84b70d3c36ccc0cb Mon Sep 17 00:00:00 2001 From: Mabel Amaya Date: Tue, 17 Feb 2026 10:33:06 -0500 Subject: [PATCH 6/8] add missing types --- packages/app/src/studio/studio-app-types.ts | 149 +++++++++++++++--- .../types/src/studio/studio-server-types.ts | 52 +++++- 2 files changed, 175 insertions(+), 26 deletions(-) diff --git a/packages/app/src/studio/studio-app-types.ts b/packages/app/src/studio/studio-app-types.ts index b701c40146..2c42d65f49 100644 --- a/packages/app/src/studio/studio-app-types.ts +++ b/packages/app/src/studio/studio-app-types.ts @@ -2,10 +2,34 @@ // `studio` bundle. It is downloaded and copied to the app. // It should not be modified directly in the app. -import type { CloudStatus } from '@cy/store/user-project-status-store' - +// Recording state: 'recording' (active), 'paused' (user-controlled, resumable), +// or 'disabled' (system-controlled, cannot be activated until conditions change). export type RecordingState = 'recording' | 'paused' | 'disabled' +export type SynchronizationMetadata = { + timestamp: number + sequence: number + frameId: string +} + +export type CloudStatus = + | 'isLoggedOut' + | 'needsOrgConnect' + | 'needsProjectConnect' + | 'needsRecordedRun' + | 'allTasksCompleted' + +export interface LoginUserData { + id: string + fullName: string | null + email: string | null +} + +export interface OrganizationData { + id: string + cypressAiDisabled?: boolean | null +} + export interface UserProjectStatusStore { user: { isLoggedIn: boolean @@ -17,6 +41,12 @@ export interface UserProjectStatusStore { openLoginConnectModal: (options: { utmMedium: string }) => void cloudStatus: CloudStatus projectId: string + userData?: LoginUserData + organization?: OrganizationData +} + +export interface AutSnapshotStore { + isSnapshotPinned: boolean } export interface RequestProjectAccessMutationResult { @@ -40,7 +70,8 @@ const SPEC_DIRTY_DATA_MODULES = Object.freeze({ }, }) -export type SpecDirtyDataModule = (typeof SPEC_DIRTY_DATA_MODULES)[keyof typeof SPEC_DIRTY_DATA_MODULES] +export type SpecDirtyDataModule = + (typeof SPEC_DIRTY_DATA_MODULES)[keyof typeof SPEC_DIRTY_DATA_MODULES] export type SpecDirtyDataModuleKey = keyof typeof SPEC_DIRTY_DATA_MODULES @@ -59,15 +90,17 @@ export interface StudioPanelProps { useRunnerStatus?: RunnerStatusShape useTestContentRetriever?: TestContentRetrieverShape useCypress?: CypressShape + useSnapshotIframe?: SnapshotIframeShape autUrlSelector?: string studioAiAvailable?: boolean - userProjectStatusStore: UserProjectStatusStore - hasRequestedProjectAccess: boolean - requestProjectAccessMutation: RequestProjectAccessMutation - specDirtyDataStore: SpecDirtyDataStore + userProjectStatusStore?: UserProjectStatusStore + hasRequestedProjectAccess?: boolean + requestProjectAccessMutation?: RequestProjectAccessMutation + specDirtyDataStore?: SpecDirtyDataStore isSelectorPlaygroundOpen?: boolean // Callback to close the Selector Playground onCloseSelectorPlayground?: () => void + autSnapshotStore?: AutSnapshotStore } export type StudioPanelShape = (props: StudioPanelProps) => JSX.Element @@ -80,28 +113,82 @@ export interface StudioAppDefaultShape { export type CypressInternal = Cypress.Cypress & CyEventEmitter & { - state: (key: string) => any + state(key: string): V + state(key: string, value: any): V + // The main AUT iframe $autIframe: JQuery + // The container for the AUT panel, which contains the frames and snapshots controls + $autPanelContainer?: JQuery + // The container for the AUT panel iframes, which contains the AUT iframe and snapshot iframes + $autIframesContainer?: JQuery + // The individual snapshot iframes rendered by Cypress for studio snapshotrendering + $autSnapshotIframes?: JQuery[] mocha: { getRootSuite: () => Suite + getRunner: () => { + stats: { + suites: number + tests: number + } + } } areSourceMapsAvailable?: boolean stackUtils?: { - getSourceDetailsForFirstLine: (stack: string, projectRoot: string) => { + getSourceDetailsForFirstLine: ( + stack: string, + projectRoot: string + ) => { line: number column: number file: string } } + // External typings do not expose the `getSelectorPriority` function on the ElementSelector object + ElementSelector: { + defaults(options: Partial): void + getSelectorPriority?: () => Cypress.SelectorPriority[] + } +} + +export type LocalRecommendationId = string + +export interface PreSettledRecommendation { + // client-side generated id, same as the counterpart pending recommendation + id: LocalRecommendationId + // server-side generated id + generationId: string + // the assertions generated by the LLM (may be updated if user edits within the recommendation) + generatedAssertions: string + // the original assertions generated by the LLM (preserved for analytics) + originalGeneratedAssertions: string + // synchronization metadata for each snapshot used to generate this recommendation + snapshotSynchronizationMetadata: SynchronizationMetadata[] + // the reason(s) for the recommendation, from the LLM response + reason?: string[] + // the node ID of the target element for this recommendation (for highlighting) + nodeId?: number } +// a settled recommendation with the assertions generated by the LLM +export interface SettledRecommendation extends PreSettledRecommendation { + // line that the recommendation starts on + startingLineNumber: number +} + +export type SettledRecommendationsById = Record< + LocalRecommendationId, + SettledRecommendation +> + export interface TestBlock { content: string testBodyPosition: { contentStart: number contentEnd: number - indentation: number + indentation: string + indentationType?: IndentationType } + recommendations?: SettledRecommendationsById } export type RunnerStatus = 'running' | 'finished' @@ -122,24 +209,12 @@ export type RunnerStatusShape = (props: RunnerStatusProps) => { runnerStatus: RunnerStatus } -export interface StudioAIStreamProps { - canAccessStudioAI: boolean - runnerStatus: RunnerStatus - testCode?: string - isCreatingNewTest: boolean - Cypress: CypressInternal -} - -export interface StudioAIStream { - recommendation: string - isStreaming: boolean - generationId: string | null -} - -export type StudioAIStreamShape = (props: StudioAIStreamProps) => StudioAIStream +export type SnapshotIframeShape = (props: CypressProps) => void export interface TestContentRetrieverProps { Cypress: CypressInternal + showCypressGrepError: boolean + isCucumberSpec: boolean } export type TestContentRetrieverShape = (props: TestContentRetrieverProps) => { @@ -157,3 +232,27 @@ export type Suite = { column: number } } + +export type IndentationType = 'space' | 'tab' + +// copied from the Cypress App +export interface AutSnapshot { + id?: number + name?: string + $el: any + snapshot?: AutSnapshot + coords: [number, number] + scrollBy: { + x: number + y: number + } + snapshots: AutSnapshot[] + highlightAttr: string + htmlAttrs: Record // Type is NamedNodeMap, not sure if we should include lib: ["DOM"] + viewportHeight: number + viewportWidth: number + url: string + body: { + get: () => unknown // TODO: find out what this is, some sort of JQuery API. + } +} diff --git a/packages/types/src/studio/studio-server-types.ts b/packages/types/src/studio/studio-server-types.ts index e9c0a2badb..310a33b1ee 100644 --- a/packages/types/src/studio/studio-server-types.ts +++ b/packages/types/src/studio/studio-server-types.ts @@ -70,6 +70,19 @@ export type BrowserWindow = { show: () => void } +export interface BrowserSession { + browserWindow: BrowserWindow + done: () => Promise + isAborted: () => boolean +} + +export interface BrowserManager { + createSession(): Promise + initialize(): Promise + destroy(): Promise + computeVisibility: boolean +} + export type StudioElectronApi = { createBrowserWindow: () => BrowserWindow } @@ -100,6 +113,7 @@ export interface StudioServerOptions { manifest?: Record verifyHash: (contents: BinaryLike, expectedHash: string) => boolean studioElectron?: StudioElectronApi + debugData?: DebugData } export interface StudioAIInitializeOptions { @@ -113,6 +127,31 @@ export interface StudioAddSocketListenersOptions { onAfterSave: (options: { error?: Error }) => void } +export type AIDisabledReason = + | 'ai_disabled_locally' + | 'browser_not_supported' + | 'studio_ai_feature_flag_disabled' + | 'no_project_slug' + | 'project_not_found' + | 'no_user' + | 'org_ai_disabled' + | 'not_org_member' + | 'not_project_member' + | 'error' + +export interface StudioConfig { + AI: { + enabled: boolean + disabledReason?: AIDisabledReason + } + organizationUuid?: string + sessionId?: string + featureFlags: { + studioNonNativeEvents: boolean + studioAI: boolean + } +} + export type StudioCDPCommands = ProtocolMapping.Commands export type StudioCDPCommand = @@ -138,11 +177,13 @@ export interface StudioCDPClient { } export interface StudioServerShape { + sessionId?: string initializeRoutes(router: Router): void canAccessStudioAI(browser: Cypress.Browser): Promise + getStudioConfig(browser: Cypress.Browser): Promise + getCachedStudioConfig(): StudioConfig addSocketListeners(options: StudioAddSocketListenersOptions | Socket): void initializeStudioAI(options: StudioAIInitializeOptions): Promise - connectToBrowser(cdpClient: StudioCDPClient): void updateSessionId(sessionId: string): void reportError( error: unknown, @@ -151,6 +192,7 @@ export interface StudioServerShape { ): void destroy(): Promise captureStudioEvent(event: StudioEvent): Promise + connectToBrowser(cdpClient: StudioCDPClient): void } export interface StudioServerDefaultShape { @@ -159,3 +201,11 @@ export interface StudioServerDefaultShape { ) => Promise MOUNT_VERSION: number } + +export type SnapshotRendererVisibilityAlgorithm = + | 'default' + | 'experimental-fast' + +export type DebugData = { + filePreprocessorHandlerText?: string +} From 07c740adeb23d82e6c001e8ed7cf2fad39b190da Mon Sep 17 00:00:00 2001 From: Mabel Amaya Date: Tue, 17 Feb 2026 11:07:56 -0500 Subject: [PATCH 7/8] fix duplicate type and ts errors --- .../data-context/src/data/ProjectConfigIpc.ts | 3 +-- .../src/data/ProjectConfigManager.ts | 3 +-- .../cloud/studio/StudioLifecycleManager.ts | 2 +- packages/server/lib/cloud/studio/studio.ts | 24 +++++++++++++------ packages/types/src/debug.ts | 7 ------ packages/types/src/index.ts | 2 -- packages/types/src/protocol.ts | 2 +- 7 files changed, 21 insertions(+), 22 deletions(-) delete mode 100644 packages/types/src/debug.ts diff --git a/packages/data-context/src/data/ProjectConfigIpc.ts b/packages/data-context/src/data/ProjectConfigIpc.ts index cb2f343ca6..fe11ec8cb8 100644 --- a/packages/data-context/src/data/ProjectConfigIpc.ts +++ b/packages/data-context/src/data/ProjectConfigIpc.ts @@ -1,7 +1,6 @@ /* eslint-disable no-dupe-class-members */ import { CypressError, getError } from '@packages/errors' -import type { DebugData } from '@packages/types/src/debug' -import type { FullConfig, TestingType } from '@packages/types' +import type { DebugData, FullConfig, TestingType } from '@packages/types' import { ChildProcess, fork, ForkOptions, spawn } from 'child_process' import EventEmitter from 'events' import path from 'path' diff --git a/packages/data-context/src/data/ProjectConfigManager.ts b/packages/data-context/src/data/ProjectConfigManager.ts index 8e57e7a774..572ea386e9 100644 --- a/packages/data-context/src/data/ProjectConfigManager.ts +++ b/packages/data-context/src/data/ProjectConfigManager.ts @@ -1,8 +1,7 @@ import { CypressError, getError } from '@packages/errors' import { PluginIpcHandler, LoadConfigReply, ProjectConfigIpc, SetupNodeEventsReply } from './ProjectConfigIpc' import assert from 'assert' -import type { DebugData } from '@packages/types/src/debug' -import type { AllModeOptions, FullConfig, TestingType } from '@packages/types' +import type { AllModeOptions, FullConfig, TestingType, DebugData } from '@packages/types' import debugLib from 'debug' import path from 'path' import _ from 'lodash' diff --git a/packages/server/lib/cloud/studio/StudioLifecycleManager.ts b/packages/server/lib/cloud/studio/StudioLifecycleManager.ts index c10729f10e..6dc1645e3f 100644 --- a/packages/server/lib/cloud/studio/StudioLifecycleManager.ts +++ b/packages/server/lib/cloud/studio/StudioLifecycleManager.ts @@ -25,7 +25,7 @@ import { INITIALIZATION_TELEMETRY_GROUP_NAMES } from './telemetry/constants/init import crypto from 'crypto' import { logError } from '@packages/stderr-filtering' import { isNonRetriableCertErrorCode } from '../network/non_retriable_cert_error_codes' -import type { DebugData } from '@packages/types/src/debug' +import type { DebugData } from '@packages/types' const debug = Debug('cypress:server:studio-lifecycle-manager') const routes = require('../routes') diff --git a/packages/server/lib/cloud/studio/studio.ts b/packages/server/lib/cloud/studio/studio.ts index b237e6079c..44fc6a0edf 100644 --- a/packages/server/lib/cloud/studio/studio.ts +++ b/packages/server/lib/cloud/studio/studio.ts @@ -1,4 +1,4 @@ -import type { StudioManagerShape, StudioStatus, StudioServerDefaultShape, StudioServerShape, ProtocolManagerShape, StudioCloudApi, StudioAIInitializeOptions, StudioEvent, StudioAddSocketListenersOptions, StudioServerOptions, StudioCDPClient } from '@packages/types' +import type { StudioManagerShape, StudioStatus, StudioServerDefaultShape, StudioServerShape, StudioConfig, ProtocolManagerShape, StudioCloudApi, StudioAIInitializeOptions, StudioEvent, StudioAddSocketListenersOptions, StudioServerOptions, StudioCDPClient } from '@packages/types' import type { Router } from 'express' import Debug from 'debug' import { requireScript } from '../require_script' @@ -6,7 +6,7 @@ import path from 'path' import crypto, { BinaryLike } from 'crypto' import { StudioElectron } from './StudioElectron' import exception from '../exception' -import type { DebugData } from '@packages/types/src/debug' +import type { DebugData } from '@packages/types' interface StudioServer { default: StudioServerDefaultShape } @@ -78,6 +78,14 @@ export class StudioManager implements StudioManagerShape { return !!(await this.invokeAsync('canAccessStudioAI', { isEssential: true }, browser)) } + async getStudioConfig (browser: Cypress.Browser): Promise { + return (await this.invokeAsync('getStudioConfig', { isEssential: true }, browser))! + } + + getCachedStudioConfig (): StudioConfig { + return this.invokeSync('getCachedStudioConfig', { isEssential: true })! + } + connectToBrowser (target: StudioCDPClient): void { if (this._studioServer) { return this.invokeSync('connectToBrowser', { isEssential: true }, target) @@ -190,11 +198,13 @@ export class StudioManager implements StudioManagerShape { } } -// Helper types for invokeSync / invokeAsync +// Helper types for invokeSync / invokeAsync (only method keys; exclude e.g. sessionId) +type StudioServerMethodKey = Exclude + type StudioServerSyncMethods = { - [K in keyof StudioServerShape]: ReturnType extends Promise ? never : K -}[keyof StudioServerShape] + [K in StudioServerMethodKey]: ReturnType extends Promise ? never : K +}[StudioServerMethodKey] type StudioServerAsyncMethods = { - [K in keyof StudioServerShape]: ReturnType extends Promise ? K : never -}[keyof StudioServerShape] + [K in StudioServerMethodKey]: ReturnType extends Promise ? K : never +}[StudioServerMethodKey] diff --git a/packages/types/src/debug.ts b/packages/types/src/debug.ts deleted file mode 100644 index a3e50f9ef2..0000000000 --- a/packages/types/src/debug.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Debug data shared by config loading, protocol manager, and studio. - * Used to surface diagnostic information (e.g. file preprocessor handler text). - */ -export interface DebugData { - filePreprocessorHandlerText?: string -} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 54d5852c14..c3ea8274b2 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -32,8 +32,6 @@ export * from './reporter' export * from './server' -export * from './debug' - export * from './util' export * from './types' diff --git a/packages/types/src/protocol.ts b/packages/types/src/protocol.ts index a0a8e18f9b..ef4f1b259c 100644 --- a/packages/types/src/protocol.ts +++ b/packages/types/src/protocol.ts @@ -2,7 +2,7 @@ import type Database from 'better-sqlite3' import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping' import type { IncomingHttpHeaders } from 'http' import type { Readable } from 'stream' -import type { DebugData } from './debug' +import type { DebugData } from './studio/studio-server-types' import type { ProxyTimings } from './proxy' import type { FoundSpec } from './spec' From 04cf4b5092dde49a9321402b89177db9b914fe39 Mon Sep 17 00:00:00 2001 From: Mabel Amaya Date: Tue, 17 Feb 2026 11:22:45 -0500 Subject: [PATCH 8/8] check for undefined and add tests --- packages/server/lib/cloud/studio/studio.ts | 16 ++++++- .../fixtures/cloud/studio/test-studio.ts | 15 ++++++- .../test/unit/cloud/studio/studio_spec.ts | 44 +++++++++++++++++++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/packages/server/lib/cloud/studio/studio.ts b/packages/server/lib/cloud/studio/studio.ts index 44fc6a0edf..2e5e5096a8 100644 --- a/packages/server/lib/cloud/studio/studio.ts +++ b/packages/server/lib/cloud/studio/studio.ts @@ -79,11 +79,23 @@ export class StudioManager implements StudioManagerShape { } async getStudioConfig (browser: Cypress.Browser): Promise { - return (await this.invokeAsync('getStudioConfig', { isEssential: true }, browser))! + const config = await this.invokeAsync('getStudioConfig', { isEssential: true }, browser) + + if (config === undefined) { + throw new Error('Studio is not available: server not initialized or an error occurred') + } + + return config } getCachedStudioConfig (): StudioConfig { - return this.invokeSync('getCachedStudioConfig', { isEssential: true })! + const config = this.invokeSync('getCachedStudioConfig', { isEssential: true }) + + if (config === undefined) { + throw new Error('Studio is not available: server not initialized or an error occurred') + } + + return config } connectToBrowser (target: StudioCDPClient): void { diff --git a/packages/server/test/support/fixtures/cloud/studio/test-studio.ts b/packages/server/test/support/fixtures/cloud/studio/test-studio.ts index c532cf8dff..46c0c9c67b 100644 --- a/packages/server/test/support/fixtures/cloud/studio/test-studio.ts +++ b/packages/server/test/support/fixtures/cloud/studio/test-studio.ts @@ -1,9 +1,14 @@ /// -import type { StudioServerShape, StudioServerDefaultShape, StudioEvent, StudioCDPClient } from '@packages/types' +import type { StudioServerShape, StudioServerDefaultShape, StudioEvent, StudioCDPClient, StudioConfig } from '@packages/types' import type { Router } from 'express' import type { Socket } from '@packages/socket' +const stubStudioConfig: StudioConfig = { + AI: { enabled: true }, + featureFlags: { studioNonNativeEvents: false, studioAI: true }, +} + class StudioServer implements StudioServerShape { initializeRoutes (router: Router): void { // This is a test implementation that does nothing @@ -13,6 +18,14 @@ class StudioServer implements StudioServerShape { return Promise.resolve(true) } + getStudioConfig (browser: Cypress.Browser): Promise { + return Promise.resolve(stubStudioConfig) + } + + getCachedStudioConfig (): StudioConfig { + return stubStudioConfig + } + initializeStudioAI (): Promise { return Promise.resolve() } diff --git a/packages/server/test/unit/cloud/studio/studio_spec.ts b/packages/server/test/unit/cloud/studio/studio_spec.ts index f87e66b11e..aef9073e4e 100644 --- a/packages/server/test/unit/cloud/studio/studio_spec.ts +++ b/packages/server/test/unit/cloud/studio/studio_spec.ts @@ -300,6 +300,50 @@ describe('lib/cloud/studio', () => { }) }) + describe('getStudioConfig and getCachedStudioConfig', () => { + const browser = { + name: 'chrome', + family: 'chromium' as const, + channel: 'stable', + displayName: 'Chrome', + version: '120.0.0', + majorVersion: '120', + path: '/path/to/chrome', + isHeaded: true, + isHeadless: false, + } + + it('getStudioConfig returns config when server is initialized', async () => { + const config = await studioManager.getStudioConfig(browser as Cypress.Browser) + + expect(config).to.have.property('AI') + expect(config.AI).to.have.property('enabled') + expect(config).to.have.property('featureFlags') + }) + + it('getStudioConfig throws when server is not initialized', async () => { + const manager = new StudioManager() + + await expect(manager.getStudioConfig(browser as Cypress.Browser)) + .to.be.rejectedWith('Studio is not available: server not initialized or an error occurred') + }) + + it('getCachedStudioConfig returns config when server is initialized', () => { + const config = studioManager.getCachedStudioConfig() + + expect(config).to.have.property('AI') + expect(config.AI).to.have.property('enabled') + expect(config).to.have.property('featureFlags') + }) + + it('getCachedStudioConfig throws when server is not initialized', () => { + const manager = new StudioManager() + + expect(() => manager.getCachedStudioConfig()) + .to.throw('Studio is not available: server not initialized or an error occurred') + }) + }) + describe('addSocketListeners', () => { it('calls addSocketListeners on the studio server', () => { sinon.stub(studio, 'addSocketListeners')