diff --git a/packages/app/src/runner/SpecRunnerOpenMode.vue b/packages/app/src/runner/SpecRunnerOpenMode.vue index b7a6a5aaf31a..884556b5f668 100644 --- a/packages/app/src/runner/SpecRunnerOpenMode.vue +++ b/packages/app/src/runner/SpecRunnerOpenMode.vue @@ -102,6 +102,7 @@ { + this.ws.emit('studio:init', ({ canAccessStudioAI, cloudStudioSessionId, error }) => { if (error) { // eslint-disable-next-line no-console console.error(error) } this.studioStore.setCanAccessStudioAI(canAccessStudioAI) + this.studioStore.setCloudStudioSessionId(cloudStudioSessionId) studioInit() }) } @@ -297,13 +298,14 @@ export class EventManager { this.reporterBus.on('studio:init:test', (testId) => { this.studioStore.setTestId(testId) - this.ws.emit('studio:init', ({ canAccessStudioAI, error }) => { + this.ws.emit('studio:init', ({ canAccessStudioAI, cloudStudioSessionId, error }) => { if (error) { // eslint-disable-next-line no-console console.error(error) } this.studioStore.setCanAccessStudioAI(canAccessStudioAI) + this.studioStore.setCloudStudioSessionId(cloudStudioSessionId) studioInit() }) }) diff --git a/packages/app/src/store/studio-store.ts b/packages/app/src/store/studio-store.ts index d6a85b0b2491..3eac0c78bfb8 100644 --- a/packages/app/src/store/studio-store.ts +++ b/packages/app/src/store/studio-store.ts @@ -122,6 +122,7 @@ interface StudioRecorderState { canAccessStudioAI: boolean showUrlPrompt: boolean + cloudStudioSessionId?: string } export const useStudioStore = defineStore('studioRecorder', { @@ -138,6 +139,7 @@ export const useStudioStore = defineStore('studioRecorder', { _currentId: 1, canAccessStudioAI: false, showUrlPrompt: true, + cloudStudioSessionId: undefined, } }, @@ -161,6 +163,10 @@ export const useStudioStore = defineStore('studioRecorder', { this.canAccessStudioAI = canAccessStudioAI }, + setCloudStudioSessionId (cloudStudioSessionId: string) { + this.cloudStudioSessionId = cloudStudioSessionId + }, + clearRunnableIds () { this.testId = undefined this.suiteId = undefined diff --git a/packages/app/src/studio/StudioPanel.vue b/packages/app/src/studio/StudioPanel.vue index a66333b58186..315bfd85b612 100644 --- a/packages/app/src/studio/StudioPanel.vue +++ b/packages/app/src/studio/StudioPanel.vue @@ -52,6 +52,7 @@ const props = defineProps<{ onStudioPanelClose: () => void eventManager: EventManager studioStatus: string | null + cloudStudioSessionId?: string }>() interface StudioApp { default: StudioAppDefaultShape } @@ -71,7 +72,11 @@ const maybeRenderReactComponent = () => { return } - const panel = window.UnifiedRunner.React.createElement(ReactStudioPanel.value, { canAccessStudioAI: props.canAccessStudioAI, onStudioPanelClose: props.onStudioPanelClose }) + const panel = window.UnifiedRunner.React.createElement(ReactStudioPanel.value, { + canAccessStudioAI: props.canAccessStudioAI, + onStudioPanelClose: props.onStudioPanelClose, + studioSessionId: props.cloudStudioSessionId, + }) if (!reactRoot.value) { reactRoot.value = window.UnifiedRunner.ReactDOM.createRoot(container.value) @@ -81,6 +86,7 @@ const maybeRenderReactComponent = () => { } watch(() => props.canAccessStudioAI, maybeRenderReactComponent) +watch(() => props.cloudStudioSessionId, maybeRenderReactComponent) const unmountReactComponent = () => { if (!ReactStudioPanel.value || !container.value) { diff --git a/packages/app/src/studio/studio-app-types.ts b/packages/app/src/studio/studio-app-types.ts index e82182690a96..eeb859abe294 100644 --- a/packages/app/src/studio/studio-app-types.ts +++ b/packages/app/src/studio/studio-app-types.ts @@ -1,6 +1,7 @@ export interface StudioPanelProps { canAccessStudioAI: boolean onStudioPanelClose?: () => void + studioSessionId?: string useRunnerStatus?: RunnerStatusShape useTestContentRetriever?: TestContentRetrieverShape useStudioAIStream?: StudioAIStreamShape diff --git a/packages/server/lib/cloud/studio/studio.ts b/packages/server/lib/cloud/studio/studio.ts index b095c9e8512c..142c884a46c4 100644 --- a/packages/server/lib/cloud/studio/studio.ts +++ b/packages/server/lib/cloud/studio/studio.ts @@ -84,6 +84,14 @@ export class StudioManager implements StudioManagerShape { await this.invokeAsync('initializeStudioAI', { isEssential: true }, options) } + updateSessionId (sessionId: string): void { + if (this._studioServer && typeof this._studioServer.updateSessionId === 'function') { + this.invokeSync('updateSessionId', { isEssential: false }, sessionId) + } else { + debug('updateSessionId method not available on studio server') + } + } + async destroy (): Promise { await this.invokeAsync('destroy', { isEssential: true }) } diff --git a/packages/server/lib/project-base.ts b/packages/server/lib/project-base.ts index 80b91df158ff..c46a7ace3c2e 100644 --- a/packages/server/lib/project-base.ts +++ b/packages/server/lib/project-base.ts @@ -418,6 +418,8 @@ export class ProjectBase extends EE { }) } + const cloudStudioSessionId = v4() + try { const isStudioReady = this.ctx.coreData.studioLifecycleManager?.isStudioReady() @@ -442,7 +444,7 @@ export class ProjectBase extends EE { endTelemetry({ status: 'studio-not-ready', canAccessStudioAI: false }) - return { canAccessStudioAI: false } + return { canAccessStudioAI: false, cloudStudioSessionId } } const studio = await this.ctx.coreData.studioLifecycleManager?.getStudio() @@ -476,7 +478,7 @@ export class ProjectBase extends EE { if (!canAccessStudioAI) { endTelemetry({ status: 'success', canAccessStudioAI }) - return { canAccessStudioAI } + return { canAccessStudioAI, cloudStudioSessionId } } this.protocolManager = studio.protocolManager @@ -495,7 +497,7 @@ export class ProjectBase extends EE { endTelemetry({ status: 'protocol-db-path-not-set', canAccessStudioAI: false }) - return { canAccessStudioAI: false } + return { canAccessStudioAI: false, cloudStudioSessionId } } telemetryManager.mark(INITIALIZATION_MARK_NAMES.INITIALIZE_STUDIO_AI_START) @@ -507,18 +509,18 @@ export class ProjectBase extends EE { endTelemetry({ status: 'success', canAccessStudioAI: true }) - return { canAccessStudioAI: true } + return { canAccessStudioAI: true, cloudStudioSessionId } } this.protocolManager = undefined endTelemetry({ status: 'success', canAccessStudioAI: false }) - return { canAccessStudioAI: false } + return { canAccessStudioAI: false, cloudStudioSessionId } } catch (error) { endTelemetry({ status: 'exception', canAccessStudioAI: false }) - return { canAccessStudioAI: false } + return { canAccessStudioAI: false, cloudStudioSessionId } } }, diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 36b77bf6f4e8..c59117a996d2 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -413,9 +413,9 @@ export class SocketBase { socket.on('studio:init', async (cb) => { try { - const { canAccessStudioAI } = await options.onStudioInit() + const { canAccessStudioAI, cloudStudioSessionId } = await options.onStudioInit() - cb({ canAccessStudioAI }) + cb({ canAccessStudioAI, cloudStudioSessionId }) } catch (error) { cb({ error: errors.cloneErr(error) }) } diff --git a/packages/server/test/unit/socket_spec.js b/packages/server/test/unit/socket_spec.js index 613cef3fbf04..10e175d1a0ff 100644 --- a/packages/server/test/unit/socket_spec.js +++ b/packages/server/test/unit/socket_spec.js @@ -542,12 +542,27 @@ describe('lib/socket', () => { context('on(studio:init)', () => { it('calls onStudioInit', async function () { - this.options.onStudioInit.resolves({ canAccessStudioAI: true }) + this.options.onStudioInit.resolves({ canAccessStudioAI: true, cloudStudioSessionId: 'test-session-id' }) await new Promise((resolve) => { - this.client.emit('studio:init', ({ canAccessStudioAI }) => { + this.client.emit('studio:init', ({ canAccessStudioAI, cloudStudioSessionId }) => { expect(this.options.onStudioInit).to.be.called expect(canAccessStudioAI).to.be.true + expect(cloudStudioSessionId).to.eq('test-session-id') + + resolve() + }) + }) + }) + + it('calls onStudioInit and handles undefined cloudStudioSessionId', async function () { + this.options.onStudioInit.resolves({ canAccessStudioAI: false, cloudStudioSessionId: undefined }) + + await new Promise((resolve) => { + this.client.emit('studio:init', ({ canAccessStudioAI, cloudStudioSessionId }) => { + expect(this.options.onStudioInit).to.be.called + expect(canAccessStudioAI).to.be.false + expect(cloudStudioSessionId).to.be.undefined resolve() }) diff --git a/packages/types/src/studio/studio-server-types.ts b/packages/types/src/studio/studio-server-types.ts index e87f6603bfe0..162caafcaa19 100644 --- a/packages/types/src/studio/studio-server-types.ts +++ b/packages/types/src/studio/studio-server-types.ts @@ -61,6 +61,7 @@ export interface StudioServerShape { canAccessStudioAI(browser: Cypress.Browser): Promise addSocketListeners(socket: Socket): void initializeStudioAI(options: StudioAIInitializeOptions): Promise + updateSessionId(sessionId: string): void reportError( error: unknown, studioMethod: string,