-
Notifications
You must be signed in to change notification settings - Fork 3.3k
chore: cy prompt infrastructure #31748
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/cy-prompt
Are you sure you want to change the base?
Changes from 9 commits
2f776b8
cd1c7e3
b843dd5
4ecb321
aca2301
bdbc924
1e1207e
7740e6b
a620d24
dcd49e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,31 @@ | ||
# `cy.prompt` Development | ||
|
||
In production, the code used to facilitate the prompt command will be retrieved from the Cloud. | ||
|
||
To run against locally developed `cy.prompt`: | ||
|
||
- Clone the `cypress-services` repo | ||
- Run `yarn` | ||
- Run `yarn watch` in `app/packages/cy-prompt` | ||
- Set: | ||
- `CYPRESS_INTERNAL_ENV=<environment>` (e.g. `staging` or `production` if you want to hit those deployments of `cypress-services` or `development` if you want to hit a locally running version of `cypress-services`) | ||
- `CYPRESS_LOCAL_CY_PROMPT_PATH` to the path to the `cypress-services/app/packages/cy-prompt/dist/development` directory | ||
- Clone the `cypress` repo | ||
- Run `yarn` | ||
- Run `yarn cypress:open` | ||
- Log In to the Cloud via the App | ||
- Open a project that has `experimentalPromptCommand: true` set in the `e2e` config of the `cypress.config.js|ts` file. | ||
|
||
To run against a deployed version of `cy.prompt`: | ||
|
||
- Set: | ||
- `CYPRESS_INTERNAL_ENV=<environment>` (e.g. `staging` or `production` if you want to hit those deployments of `cypress-services` or `development` if you want to hit a locally running version of `cypress-services`) | ||
``` | ||
|
||
## Testing | ||
|
||
### Unit/Component Testing | ||
|
||
The code that supports cloud `cy.prompt` and lives in the `cypress` monorepo is unit, integration, and e2e tested in a similar fashion to the rest of the code in the repo. See the [contributing guide](https://github.com/cypress-io/cypress/blob/ad353fcc0f7fdc51b8e624a2a1ef4e76ef9400a0/CONTRIBUTING.md?plain=1#L366) for more specifics. | ||
|
||
The code that supports cloud `cy.prompt` and lives in the `cypress-services` monorepo has unit tests that live alongside the code in that monorepo. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
describe('src/cy/commands/prompt', () => { | ||
it('executes the prompt command', () => { | ||
cy.visit('/fixtures/dom.html') | ||
|
||
// TODO: add more tests when cy.prompt is built out, but for now this just | ||
// verifies that the command executes without throwing an error | ||
cy.prompt('Hello, world!') | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { init, loadRemote } from '@module-federation/runtime' | ||
import type{ CyPromptDriverDefaultShape } from './prompt-driver-types' | ||
|
||
interface CyPromptDriver { default: CyPromptDriverDefaultShape } | ||
|
||
let initializedCyPrompt: CyPromptDriverDefaultShape | null = null | ||
const initializeCloudCyPrompt = async (Cypress: Cypress.Cypress): Promise<CyPromptDriverDefaultShape> => { | ||
mschile marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const { success } = await Cypress.backend('wait:for:cy:prompt:ready') | ||
|
||
if (!success) { | ||
throw new Error('CyPromptDriver not found') | ||
} | ||
|
||
init({ | ||
remotes: [{ | ||
alias: 'cy-prompt', | ||
type: 'module', | ||
name: 'cy-prompt', | ||
entryGlobalName: 'cy-prompt', | ||
entry: '/__cypress-cy-prompt/cy-prompt.js', | ||
shareScope: 'default', | ||
}], | ||
name: 'driver', | ||
}) | ||
|
||
const module = await loadRemote<CyPromptDriver>('cy-prompt') | ||
|
||
if (!module?.default) { | ||
throw new Error('CyPromptDriver not found') | ||
} | ||
|
||
initializedCyPrompt = module.default | ||
|
||
return module.default | ||
} | ||
|
||
export default (Commands, Cypress, cy) => { | ||
Commands.addAll({ | ||
async prompt (message: string) { | ||
if (!Cypress.config('experimentalPromptCommand')) { | ||
// TODO: what do we want to do here? | ||
throw new Error('cy.prompt() is not enabled. Please enable it by setting `experimentalPromptCommand: true` in your Cypress config.') | ||
} | ||
|
||
try { | ||
let cloud = initializedCyPrompt | ||
|
||
if (!cloud) { | ||
cloud = await initializeCloudCyPrompt(Cypress) | ||
} | ||
|
||
return await cloud.cyPrompt(Cypress, message) | ||
} catch (error) { | ||
// TODO: handle this better | ||
throw new Error('CyPromptDriver not found') | ||
} | ||
}, | ||
}) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export interface CypressInternal extends Cypress.Cypress { | ||
backend: (eventName: string, ...args: any[]) => Promise<any> | ||
ryanthemanuel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
export interface CyPromptDriverDefaultShape { | ||
cyPrompt: (Cypress: CypressInternal, text: string) => Promise<void> | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,64 @@ | ||||||||||||||||||||||||||||||||||
import { asyncRetry, linearDelay } from '../../../util/async_retry' | ||||||||||||||||||||||||||||||||||
import { isRetryableError } from '../../network/is_retryable_error' | ||||||||||||||||||||||||||||||||||
import fetch from 'cross-fetch' | ||||||||||||||||||||||||||||||||||
import os from 'os' | ||||||||||||||||||||||||||||||||||
import { agent } from '@packages/network' | ||||||||||||||||||||||||||||||||||
import { PUBLIC_KEY_VERSION } from '../../constants' | ||||||||||||||||||||||||||||||||||
import { createWriteStream } from 'fs' | ||||||||||||||||||||||||||||||||||
import { verifySignatureFromFile } from '../../encryption' | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
const pkg = require('@packages/root') | ||||||||||||||||||||||||||||||||||
const _delay = linearDelay(500) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
export const getCyPromptBundle = async ({ cyPromptUrl, projectId, bundlePath }: { cyPromptUrl: string, projectId: string, bundlePath: string }) => { | ||||||||||||||||||||||||||||||||||
let responseSignature: string | null = null | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
await (asyncRetry(async () => { | ||||||||||||||||||||||||||||||||||
const response = await fetch(cyPromptUrl, { | ||||||||||||||||||||||||||||||||||
// @ts-expect-error - this is supported | ||||||||||||||||||||||||||||||||||
agent, | ||||||||||||||||||||||||||||||||||
method: 'GET', | ||||||||||||||||||||||||||||||||||
headers: { | ||||||||||||||||||||||||||||||||||
'x-route-version': '1', | ||||||||||||||||||||||||||||||||||
'x-cypress-signature': PUBLIC_KEY_VERSION, | ||||||||||||||||||||||||||||||||||
...(projectId ? { 'x-cypress-project-slug': projectId } : {}), | ||||||||||||||||||||||||||||||||||
'x-cypress-cy-prompt-mount-version': '1', | ||||||||||||||||||||||||||||||||||
'x-os-name': os.platform(), | ||||||||||||||||||||||||||||||||||
'x-cypress-version': pkg.version, | ||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||
encrypt: 'signed', | ||||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
if (!response.ok) { | ||||||||||||||||||||||||||||||||||
throw new Error(`Failed to download cy-prompt bundle: ${response.statusText}`) | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
responseSignature = response.headers.get('x-cypress-signature') | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
await new Promise<void>((resolve, reject) => { | ||||||||||||||||||||||||||||||||||
const writeStream = createWriteStream(bundlePath) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
writeStream.on('error', reject) | ||||||||||||||||||||||||||||||||||
writeStream.on('finish', () => { | ||||||||||||||||||||||||||||||||||
resolve() | ||||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
// @ts-expect-error - this is supported | ||||||||||||||||||||||||||||||||||
response.body?.pipe(writeStream) | ||||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||||
}, { | ||||||||||||||||||||||||||||||||||
maxAttempts: 3, | ||||||||||||||||||||||||||||||||||
retryDelay: _delay, | ||||||||||||||||||||||||||||||||||
shouldRetry: isRetryableError, | ||||||||||||||||||||||||||||||||||
}))() | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
if (!responseSignature) { | ||||||||||||||||||||||||||||||||||
throw new Error('Unable to get studio signature') | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
const verified = await verifySignatureFromFile(bundlePath, responseSignature) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
if (!verified) { | ||||||||||||||||||||||||||||||||||
throw new Error('Unable to verify studio signature') | ||||||||||||||||||||||||||||||||||
Comment on lines
+56
to
+62
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error message below refers to 'studio signature' but this is for cy-prompt; update it to reference 'cy-prompt signature' to avoid confusion.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error thrown here mentions 'Unable to verify studio signature'—it should reference 'cy-prompt signature' for accuracy.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is an unmatched code fence here that breaks the markdown list; remove or properly close the block so the document renders correctly.
Copilot uses AI. Check for mistakes.