From 3406a9e854e2cc2e5e7e4e80ee6ce71fef9780e8 Mon Sep 17 00:00:00 2001 From: yyh Date: Fri, 10 Jul 2026 16:25:07 +0800 Subject: [PATCH] test(e2e): cover agent speech-to-text --- e2e/AGENTS.md | 3 + e2e/features/agent-v2/AGENTS.md | 7 ++ e2e/features/agent-v2/preflight.feature | 5 + e2e/features/agent-v2/speech-to-text.feature | 12 ++ .../support/agent-builder-resources.ts | 1 + e2e/features/agent-v2/support/agent-soul.ts | 14 +++ .../agent-v2/support/preflight/models.ts | 44 ++++++- e2e/features/agent-v2/support/seed.ts | 110 ++++++++++++++++-- .../agent-v2/preflight.steps.ts | 9 ++ .../agent-v2/speech-to-text.steps.ts | 92 +++++++++++++++ e2e/features/support/hooks.ts | 39 ++++++- e2e/features/support/world.ts | 9 ++ e2e/fixtures/test-materials/voice-input.wav | Bin 0 -> 256078 bytes e2e/support/test-materials.ts | 6 + 14 files changed, 339 insertions(+), 12 deletions(-) create mode 100644 e2e/features/agent-v2/speech-to-text.feature create mode 100644 e2e/features/step-definitions/agent-v2/speech-to-text.steps.ts create mode 100644 e2e/fixtures/test-materials/voice-input.wav diff --git a/e2e/AGENTS.md b/e2e/AGENTS.md index 6f41c0e921e..d5bcb0bc868 100644 --- a/e2e/AGENTS.md +++ b/e2e/AGENTS.md @@ -209,10 +209,13 @@ Feature: Create dataset - `@fresh` — only runs in `e2e:full` mode (requires uninitialized instance) - `@external-model` — scenario execution can call a real model provider. Use this only for runtime requests, not for scenarios that only require an active model fixture. - `@external-tool` — scenario execution can call a real third-party tool provider. Use this only for runtime tool execution, not for plugin installation, discovery, or local deterministic tools. +- `@microphone` — runs the scenario in an isolated Chromium instance backed by the checked-in fake audio fixture and grants microphone permission only to that scenario context. - `@skip` — excluded from all runs External runtime commands are opt-in. `pnpm -C e2e e2e:external:prepare` reads `E2E_EXTERNAL_RUNTIME_SEED_SPECS`, defaulting to `agent-v2:external-runtime`, and runs the matching seed packs before the external suite. `pnpm -C e2e e2e:external` reads `E2E_EXTERNAL_RUNTIME_TAGS`, defaulting to `(@external-model or @external-tool) and not @feature-gated and not @skip and not @preview`. +The Agent v2 external runtime seed also prepares the workspace default Speech-to-Text model. `E2E_SPEECH_TO_TEXT_MODEL_PROVIDER` and `E2E_SPEECH_TO_TEXT_MODEL_NAME` select an existing model or the model configured through `E2E_MODEL_PROVIDER_CREDENTIALS_JSON`; they default to `openai` and `gpt-4o-mini-transcribe`. + Some external runtime scenarios need feature-owned services in addition to a real model or tool provider. Do not overload `@external-model` or `@external-tool` to mean those services are available. For Agent v2, scenarios that require the standalone `dify-agent` run server use the feature tag `@agent-backend-runtime` plus the explicit step `the Agent v2 runtime backend is available`. Run them with `E2E_START_AGENT_BACKEND=1` to let E2E start `dify-agent` and the shellctl local sandbox required by its `dify.config`/`dify.shell` runtime layers, or set `E2E_AGENT_BACKEND_URL`/`AGENT_BACKEND_BASE_URL` when an existing server should be reused. Keep scenarios short and declarative. Each step should describe **what** the user does, not **how** the UI works. diff --git a/e2e/features/agent-v2/AGENTS.md b/e2e/features/agent-v2/AGENTS.md index 9f8081970fc..5c45561fafb 100644 --- a/e2e/features/agent-v2/AGENTS.md +++ b/e2e/features/agent-v2/AGENTS.md @@ -36,6 +36,8 @@ Use tags in three layers: - `@publish` — publish and publish-bar state. - `@access-point` — Web app, Backend service API, and Workflow access surfaces. - `@stable-model` — active model fixture dependency. Apply this to every scenario that includes `the Agent Builder stable chat model is available` or otherwise requires an active model configured in the workspace. +- `@speech-to-text-model` — active workspace default Speech-to-Text model dependency. Apply this to scenarios that include `the workspace default speech-to-text model is active`. +- `@microphone` — deterministic Chromium fake microphone dependency. The scenario hook launches a fake-audio browser and grants microphone permission only to tagged scenarios. - `@agent-decision-model` — stronger active model fixture dependency for scenarios that specifically validate Agent autonomous planning or resource-selection behavior, such as generated-query Knowledge Retrieval. Do not use it for scenarios that only need a model to exist or answer a deterministic prompt. - `@tool-fixture` — preseeded Tool dependency such as `JSON Process / JSON Replace` or `Tavily / Tavily Search`. - `@skill-fixture` — checked-in or preseeded Skill dependency such as `e2e-summary-skill`. @@ -75,6 +77,7 @@ Keep Agent v2 step definitions grouped by user capability, not by DOM component - `access-point-service-api.steps.ts` — Backend service API entrypoints, keys, API reference, and service requests. - `access-point-workflow.steps.ts` — Workflow access references. - `preflight.steps.ts` — explicit `Given` entrypoints for Agent Builder preflight resources. +- `speech-to-text.steps.ts` — Agent Build voice input, multipart request, and transcribed input behavior. Cucumber step definitions are globally registered. Do not duplicate the same step text across files, even if one is written as `Given` and another as `Then`. @@ -87,6 +90,7 @@ Agent v2 business state belongs under `world.agentBuilder`; do not keep adding A Use the existing namespace shape: - `world.agentBuilder.preflight.stableModel` +- `world.agentBuilder.preflight.speechToTextModel` - `world.agentBuilder.preflight.brokenModel` - `world.agentBuilder.preflight.preseededResources` - `world.agentBuilder.accessPoint.serviceApiBaseURL` @@ -98,6 +102,7 @@ Use the existing namespace shape: - `world.agentBuilder.accessPoint.workflowReferencePage` - `world.agentBuilder.accessPoint.composerDraftSnapshot` - `world.agentBuilder.configure.concurrentPage` +- `world.agentBuilder.speechToText.request` - `world.agentBuilder.workflow.agentConsolePage` - `world.agentBuilder.workflow.outputVariables` @@ -163,6 +168,8 @@ Use `the Agent v2 runtime backend is available` before scenarios tagged `@agent- Use `the Agent Builder stable chat model is available` before scenarios that need a real Agent Soul model configuration. This includes true runtime scenarios, model-backed build-mode assertions, and Workflow Agent v2 node setup because the backend rejects Agent nodes without model config. Do not add the model preflight to pure navigation or identity checks unless the setup API itself requires model config. `E2E_STABLE_MODEL_PROVIDER`, `E2E_STABLE_MODEL_NAME`, and optional `E2E_STABLE_MODEL_TYPE` are selectors for a model already configured in the workspace; they are not provider credentials. The step defaults to `openai` / `gpt-5-nano` / `llm`, verifies the selected model is present and `active` through `/console/api/workspaces/current/models/model-types/{type}`, then stores it on `DifyWorld.agentBuilder.preflight.stableModel`. +Use `the workspace default speech-to-text model is active` before real speech-to-text scenarios. The preflight reads `/console/api/workspaces/current/default-model?model_type=speech2text`, verifies that same provider/model is active in the Speech-to-Text model list, and stores it on `DifyWorld.agentBuilder.preflight.speechToTextModel`. It must not configure a model or provider credential. External runtime preparation selects `E2E_SPEECH_TO_TEXT_MODEL_PROVIDER` / `E2E_SPEECH_TO_TEXT_MODEL_NAME`, defaulting to `openai` / `gpt-4o-mini-transcribe`, reuses `E2E_MODEL_PROVIDER_CREDENTIALS_JSON` when provider setup is required, and selects the active model as the workspace default. + Use `the Agent Builder agent-decision chat model is available` before scenarios that need a stronger model to exercise Agent autonomous planning, generated query selection, or tool/resource choice. `E2E_AGENT_DECISION_MODEL_PROVIDER`, `E2E_AGENT_DECISION_MODEL_NAME`, and optional `E2E_AGENT_DECISION_MODEL_TYPE` are selectors for a second active model fixture, defaulting to `openai` / `gpt-5.5` / `llm`. The step stores the model on `DifyWorld.agentBuilder.preflight.agentDecisionModel`. Do not use this fixture as a broad replacement for `@stable-model`; it is intentionally narrower and costlier. Keep `@stable-model` on Build draft apply scenarios that click `Apply`. The current product path calls `/build-chat/finalize` before applying the draft, and the backend returns `model is required` when the Agent Soul has no model config. Discard-only and pending-draft isolation scenarios can stay model-free when they do not finalize the Build draft. diff --git a/e2e/features/agent-v2/preflight.feature b/e2e/features/agent-v2/preflight.feature index aa130331304..6450f7fbdf9 100644 --- a/e2e/features/agent-v2/preflight.feature +++ b/e2e/features/agent-v2/preflight.feature @@ -14,6 +14,11 @@ Feature: Agent Builder preseeded environment Given I am signed in as the default E2E admin And the Agent Builder stable chat model is available + @speech-to-text-model + Scenario: Default speech-to-text model is available + Given I am signed in as the default E2E admin + And the workspace default speech-to-text model is active + @agent-decision-model Scenario: Agent-decision chat model is available Given I am signed in as the default E2E admin diff --git a/e2e/features/agent-v2/speech-to-text.feature b/e2e/features/agent-v2/speech-to-text.feature new file mode 100644 index 00000000000..d5f4d87a52b --- /dev/null +++ b/e2e/features/agent-v2/speech-to-text.feature @@ -0,0 +1,12 @@ +@agent-v2 @authenticated +Feature: Agent v2 speech-to-text + @build @speech-to-text @microphone @external-model @speech-to-text-model + Scenario: Recorded speech is transcribed into the current Agent input + Given I am signed in as the default E2E admin + And the workspace default speech-to-text model is active + And an Agent v2 test agent with speech-to-text enabled has been created via API + When I open the Agent v2 configure page + And I start Agent v2 voice input + And I stop Agent v2 voice input after the fixture speech has played + Then the Agent v2 speech-to-text request should succeed + And the transcribed fixture phrase "Purple elephant seven" should appear in the Agent v2 input diff --git a/e2e/features/agent-v2/support/agent-builder-resources.ts b/e2e/features/agent-v2/support/agent-builder-resources.ts index 9b296ecf34e..776063fc0d5 100644 --- a/e2e/features/agent-v2/support/agent-builder-resources.ts +++ b/e2e/features/agent-v2/support/agent-builder-resources.ts @@ -1,5 +1,6 @@ export const agentBuilderPreseededResources = { stableChatModel: 'E2E Stable Chat Model', + speechToTextModel: 'Workspace default Speech-to-Text model', summarySkill: 'e2e-summary-skill', jsonReplaceTool: 'JSON Process / JSON Replace', tavilySearchTool: 'Tavily / Tavily Search', diff --git a/e2e/features/agent-v2/support/agent-soul.ts b/e2e/features/agent-v2/support/agent-soul.ts index 4697234b83c..d25fa8978a1 100644 --- a/e2e/features/agent-v2/support/agent-soul.ts +++ b/e2e/features/agent-v2/support/agent-soul.ts @@ -86,6 +86,20 @@ export function createAgentSoulConfigWithModel( } } +export function createAgentSoulConfigWithSpeechToText( + agentSoul: AgentSoulConfig, +): AgentSoulConfig { + return { + ...agentSoul, + app_features: { + ...agentSoul.app_features, + speech_to_text: { + enabled: true, + }, + }, + } +} + export function createPublishableAgentSoulConfig(agentSoul: AgentSoulConfig): AgentSoulConfig { if (agentSoul.model) return agentSoul diff --git a/e2e/features/agent-v2/support/preflight/models.ts b/e2e/features/agent-v2/support/preflight/models.ts index 02f032c4b91..a24bb00bac9 100644 --- a/e2e/features/agent-v2/support/preflight/models.ts +++ b/e2e/features/agent-v2/support/preflight/models.ts @@ -1,4 +1,7 @@ -import type { ProviderWithModelsResponse } from '@dify/contracts/api/console/workspaces/types.gen' +import type { + DefaultModelDataResponse, + ProviderWithModelsResponse, +} from '@dify/contracts/api/console/workspaces/types.gen' import type { DifyWorld } from '../../../support/world' import { createApiContext, expectApiResponseOK } from '../../../../support/api' import { agentBuilderPreseededResources } from '../agent-builder-resources' @@ -152,6 +155,45 @@ export async function skipMissingAgentBuilderStableChatModel( }) } +export async function skipMissingAgentBuilderSpeechToTextModel( + world: DifyWorld, +): Promise<'skipped' | NonNullable> { + const ctx = await createApiContext() + let defaultModel: NonNullable + + try { + const response = await ctx.get( + '/console/api/workspaces/current/default-model?model_type=speech2text', + ) + await expectApiResponseOK(response, `Check ${agentBuilderPreseededResources.speechToTextModel}`) + const body = (await response.json()) as DefaultModelDataResponse + if (!body.data) { + return skipBlockedPrecondition( + world, + `${agentBuilderPreseededResources.speechToTextModel} is not configured.`, + { + owner: 'model-provider/seed', + remediation: 'Configure an active workspace default Speech-to-Text model before running the external scenario.', + }, + ) + } + defaultModel = body.data + } + finally { + await ctx.dispose() + } + + return skipMissingAgentBuilderModel(world, { + ok: true, + provider: defaultModel.provider.provider, + resourceName: agentBuilderPreseededResources.speechToTextModel, + type: 'speech2text', + value: defaultModel.model, + }, { + requireActive: true, + }) +} + export async function skipMissingAgentBuilderAgentDecisionChatModel( world: DifyWorld, ): Promise<'skipped' | NonNullable> { diff --git a/e2e/features/agent-v2/support/seed.ts b/e2e/features/agent-v2/support/seed.ts index 0ef68c9f5ac..c9d2568279c 100644 --- a/e2e/features/agent-v2/support/seed.ts +++ b/e2e/features/agent-v2/support/seed.ts @@ -8,6 +8,7 @@ import type { } from '@dify/contracts/api/console/datasets/types.gen' import type { AvailableModelListResponse, + DefaultModelDataResponse, ModelProviderListResponse, } from '@dify/contracts/api/console/workspaces/types.gen' import type { SeedContext, SeedResource, SeedTask } from '../../../support/seed' @@ -79,6 +80,8 @@ type ToolResource = SeedResource & { } const modelCredentialEnv = 'E2E_MODEL_PROVIDER_CREDENTIALS_JSON' +const speechToTextModelProviderEnv = 'E2E_SPEECH_TO_TEXT_MODEL_PROVIDER' +const speechToTextModelNameEnv = 'E2E_SPEECH_TO_TEXT_MODEL_NAME' const marketplacePluginIdsEnv = 'E2E_MARKETPLACE_PLUGIN_IDS' const marketplacePluginUniqueIdentifiersEnv = 'E2E_MARKETPLACE_PLUGIN_UNIQUE_IDENTIFIERS' const oauthToolCredentialIdEnv = 'E2E_OAUTH_TOOL_CREDENTIAL_ID' @@ -114,6 +117,12 @@ const agentDecisionModelConfig = (): StableModel => ({ type: process.env.E2E_AGENT_DECISION_MODEL_TYPE?.trim() || 'llm', }) +const speechToTextModelConfig = (): StableModel => ({ + name: process.env[speechToTextModelNameEnv]?.trim() || 'gpt-4o-mini-transcribe', + provider: process.env[speechToTextModelProviderEnv]?.trim() || 'openai', + type: 'speech2text', +}) + const parseJsonEnv = (envName: string) => { const raw = process.env[envName]?.trim() if (!raw) @@ -134,7 +143,7 @@ const parseJsonEnv = (envName: string) => { } } -const findChatModel = async (config: StableModel, title: string) => { +const findModel = async (config: StableModel, title: string) => { const ctx = await createApiContext() try { const response = await ctx.get(`/console/api/workspaces/current/models/model-types/${config.type}`) @@ -248,14 +257,14 @@ const upsertStableProviderCredential = async ( } } -const seedChatModel = async (context: SeedContext, { +const seedModel = async (context: SeedContext, { config, title, }: { config: StableModel title: string }) => { - const existing = await findChatModel(config, title) + const existing = await findModel(config, title) const resource = { id: `${existing?.provider ?? config.provider}/${existing?.name ?? config.name}`, kind: 'model', @@ -313,7 +322,7 @@ const seedChatModel = async (context: SeedContext, { } } - const seeded = await findChatModel(config, title) + const seeded = await findModel(config, title) if (seeded?.status !== activeModelStatus) { return blocked( title, @@ -328,16 +337,94 @@ const seedChatModel = async (context: SeedContext, { }) } -const seedStableModel = async (context: SeedContext) => seedChatModel(context, { +const seedStableModel = async (context: SeedContext) => seedModel(context, { config: stableModelConfig(), title: agentBuilderPreseededResources.stableChatModel, }) -const seedAgentDecisionModel = async (context: SeedContext) => seedChatModel(context, { +const seedAgentDecisionModel = async (context: SeedContext) => seedModel(context, { config: agentDecisionModelConfig(), title: agentBuilderPreseededResources.agentDecisionChatModel, }) +const getDefaultModel = async (modelType: string) => { + const ctx = await createApiContext() + try { + const response = await ctx.get( + `/console/api/workspaces/current/default-model?${buildQuery({ model_type: modelType })}`, + ) + await expectApiResponseOK(response, `Get default ${modelType} model`) + const body = (await response.json()) as DefaultModelDataResponse + return body.data + } + finally { + await ctx.dispose() + } +} + +const setDefaultModel = async (model: StableModel) => { + const ctx = await createApiContext() + try { + const response = await ctx.post('/console/api/workspaces/current/default-model', { + data: { + model_settings: [{ + model: model.name, + model_type: model.type, + provider: model.provider, + }], + }, + }) + await expectApiResponseOK(response, `Set default ${model.type} model`) + } + finally { + await ctx.dispose() + } +} + +const seedSpeechToTextModel = async (context: SeedContext) => { + const config = speechToTextModelConfig() + const title = agentBuilderPreseededResources.speechToTextModel + const modelResult = await seedModel(context, { config, title }) + if (modelResult.status === 'blocked' || modelResult.status === 'skipped') + return modelResult + + const model = await findModel(config, title) + if (!model || model.status !== activeModelStatus) + return blocked(title, `${config.provider}/${config.name} is not active after model setup.`) + + const resource = { + id: `${model.provider}/${model.name}`, + kind: 'model', + name: title, + } + const defaultModel = await getDefaultModel(config.type) + const isExpectedDefault = defaultModel?.model === model.name + && matchesProvider(defaultModel.provider.provider, model.provider) + + if (isExpectedDefault) + return modelResult.status === 'updated' ? modelResult : verified(title, resource) + + if (context.dryRun) + return skipped(title, `Would set ${model.provider}/${model.name} as the workspace default Speech-to-Text model.`) + + await setDefaultModel({ + name: model.name, + provider: model.provider, + type: config.type, + }) + + const updatedDefaultModel = await getDefaultModel(config.type) + if (updatedDefaultModel?.model !== model.name + || !matchesProvider(updatedDefaultModel.provider.provider, model.provider)) { + return blocked( + title, + `${model.provider}/${model.name} was not selected as the workspace default Speech-to-Text model.`, + ) + } + + return updated(title, resource) +} + type BuiltinToolProvider = { label?: { en_US?: string, zh_Hans?: string } name: string @@ -1011,12 +1098,21 @@ const agentV2FullSeedTasks = (): SeedTask[] => [ }, ] +const agentV2ExternalRuntimeSeedTasks = (): SeedTask[] => [ + ...agentV2BaseSeedTasks(), + { + id: 'speech-to-text-model', + title: agentBuilderPreseededResources.speechToTextModel, + run: seedSpeechToTextModel, + }, +] + export const createAgentV2SeedTasks = (profile: string = 'full'): SeedTask[] => { if (profile === 'full') return agentV2FullSeedTasks() if (profile === 'external-runtime') - return agentV2BaseSeedTasks() + return agentV2ExternalRuntimeSeedTasks() throw new Error(`Unknown Agent V2 seed profile "${profile}".`) } diff --git a/e2e/features/step-definitions/agent-v2/preflight.steps.ts b/e2e/features/step-definitions/agent-v2/preflight.steps.ts index 9748680449e..bed2e62f208 100644 --- a/e2e/features/step-definitions/agent-v2/preflight.steps.ts +++ b/e2e/features/step-definitions/agent-v2/preflight.steps.ts @@ -23,6 +23,7 @@ import { import { skipMissingAgentBuilderAgentDecisionChatModel, skipMissingAgentBuilderBrokenChatModel, + skipMissingAgentBuilderSpeechToTextModel, skipMissingAgentBuilderStableChatModel, } from '../../agent-v2/support/preflight/models' import { skipMissingPreseededTool } from '../../agent-v2/support/preflight/tools' @@ -35,6 +36,14 @@ Given('the Agent Builder stable chat model is available', async function (this: this.agentBuilder.preflight.stableModel = stableModel }) +Given('the workspace default speech-to-text model is active', async function (this: DifyWorld) { + const speechToTextModel = await skipMissingAgentBuilderSpeechToTextModel(this) + if (speechToTextModel === 'skipped') + return speechToTextModel + + this.agentBuilder.preflight.speechToTextModel = speechToTextModel +}) + Given('the Agent Builder agent-decision chat model is available', async function (this: DifyWorld) { const agentDecisionModel = await skipMissingAgentBuilderAgentDecisionChatModel(this) if (agentDecisionModel === 'skipped') diff --git a/e2e/features/step-definitions/agent-v2/speech-to-text.steps.ts b/e2e/features/step-definitions/agent-v2/speech-to-text.steps.ts new file mode 100644 index 00000000000..f45d721bcb3 --- /dev/null +++ b/e2e/features/step-definitions/agent-v2/speech-to-text.steps.ts @@ -0,0 +1,92 @@ +import type { DifyWorld } from '../../support/world' +import { Given, Then, When } from '@cucumber/cucumber' +import { expect } from '@playwright/test' +import { voiceInputTestMaterial } from '../../../support/test-materials' +import { createConfiguredTestAgent } from '../../agent-v2/support/agent' +import { + createAgentSoulConfigWithSpeechToText, + normalAgentSoulConfig, +} from '../../agent-v2/support/agent-soul' +import { getCurrentAgentId } from './configure-helpers' + +const getAgentInput = (world: DifyWorld) => + world.getPage().getByPlaceholder('Describe what your agent should do') + +const escapeRegExp = (value: string) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&') + +Given( + 'an Agent v2 test agent with speech-to-text enabled has been created via API', + async function (this: DifyWorld) { + if (!this.agentBuilder.preflight.speechToTextModel) { + throw new Error( + 'Create a speech-to-text Agent v2 test agent after the default Speech-to-Text model preflight.', + ) + } + + const agent = await createConfiguredTestAgent({ + agentSoul: createAgentSoulConfigWithSpeechToText(normalAgentSoulConfig), + }) + this.createdAgentIds.push(agent.id) + this.lastCreatedAgentName = agent.name + this.lastCreatedAgentRole = agent.role ?? undefined + }, +) + +When('I start Agent v2 voice input', async function (this: DifyWorld) { + const page = this.getPage() + + await expect(getAgentInput(this)).toBeVisible({ timeout: 30_000 }) + await page.getByRole('button', { name: 'Voice input' }).click() + await expect(page.getByText('Speak now...')).toBeVisible({ timeout: 30_000 }) +}) + +When( + 'I stop Agent v2 voice input after the fixture speech has played', + async function (this: DifyWorld) { + const page = this.getPage() + const agentId = getCurrentAgentId(this) + + await expect(page.getByTestId('voice-input-timer')).toHaveText( + voiceInputTestMaterial.recordingDuration, + { timeout: 15_000 }, + ) + + const responsePromise = page.waitForResponse(response => ( + response.request().method() === 'POST' + && new URL(response.url()).pathname === `/console/api/agent/${agentId}/audio-to-text` + )) + await page.getByTestId('voice-input-stop').click() + const response = await responsePromise + const request = response.request() + + this.agentBuilder.speechToText.request = { + contentType: request.headers()['content-type'] ?? '', + path: new URL(response.url()).pathname, + status: response.status(), + } + }, +) + +Then('the Agent v2 speech-to-text request should succeed', async function (this: DifyWorld) { + const request = this.agentBuilder.speechToText.request + if (!request) + throw new Error('No Agent v2 speech-to-text request was captured.') + + expect(request).toEqual(expect.objectContaining({ + contentType: expect.stringContaining('multipart/form-data'), + path: `/console/api/agent/${getCurrentAgentId(this)}/audio-to-text`, + status: 200, + })) +}) + +Then( + 'the transcribed fixture phrase {string} should appear in the Agent v2 input', + async function (this: DifyWorld, expectedPhrase: string) { + const phrasePattern = new RegExp( + expectedPhrase.trim().split(/\s+/).map(escapeRegExp).join('\\s+'), + 'i', + ) + + await expect(getAgentInput(this)).toHaveValue(phrasePattern, { timeout: 60_000 }) + }, +) diff --git a/e2e/features/support/hooks.ts b/e2e/features/support/hooks.ts index 658f3efc69b..c905aaabb4d 100644 --- a/e2e/features/support/hooks.ts +++ b/e2e/features/support/hooks.ts @@ -9,6 +9,7 @@ import { chromium } from '@playwright/test' import { AUTH_BOOTSTRAP_TIMEOUT_MS, ensureAuthenticatedState } from '../../fixtures/auth' import { deleteTestApp } from '../../support/api' import { deleteTestDataset } from '../../support/datasets' +import { getVoiceInputTestMaterialPath } from '../../support/test-materials' import { deleteBuiltinToolCredential } from '../../support/tools' import { baseURL, cucumberHeadless, cucumberSlowMo } from '../../test-env' import { deleteTestAgent } from '../agent-v2/support/agent' @@ -18,6 +19,7 @@ const e2eRoot = fileURLToPath(new URL('../..', import.meta.url)) const artifactsDir = path.join(e2eRoot, 'cucumber-report', 'artifacts') let browser: Browser | undefined +let microphoneBrowserPromise: Promise | undefined setDefaultTimeout(60_000) @@ -103,19 +105,45 @@ BeforeAll({ timeout: AUTH_BOOTSTRAP_TIMEOUT_MS }, async () => { await ensureAuthenticatedState(browser, baseURL) }) +const getMicrophoneBrowser = () => { + microphoneBrowserPromise ??= chromium.launch({ + args: [ + '--use-fake-device-for-media-stream', + '--use-fake-ui-for-media-stream', + `--use-file-for-fake-audio-capture=${getVoiceInputTestMaterialPath()}%noloop`, + ], + headless: cucumberHeadless, + slowMo: cucumberSlowMo, + }) + + return microphoneBrowserPromise +} + Before(async function (this: DifyWorld, { pickle }) { if (!browser) throw new Error('Shared Playwright browser is not available.') - const isUnauthenticatedScenario = pickle.tags.some(tag => tag.name === '@unauthenticated') + const scenarioTags = pickle.tags.map(tag => tag.name) + const isMicrophoneScenario = scenarioTags.includes('@microphone') + const isUnauthenticatedScenario = scenarioTags.includes('@unauthenticated') + const scenarioBrowser = isMicrophoneScenario ? await getMicrophoneBrowser() : browser if (isUnauthenticatedScenario) - await this.startUnauthenticatedSession(browser) - else await this.startAuthenticatedSession(browser) + await this.startUnauthenticatedSession(scenarioBrowser) + else await this.startAuthenticatedSession(scenarioBrowser) + + if (isMicrophoneScenario) { + if (!this.context) + throw new Error('Playwright context has not been initialized for the microphone scenario.') + + await this.context.grantPermissions(['microphone'], { + origin: new URL(baseURL).origin, + }) + } this.scenarioStartedAt = Date.now() - const tags = pickle.tags.map(tag => tag.name).join(' ') + const tags = scenarioTags.join(' ') console.warn(`[e2e] start ${pickle.name}${tags ? ` ${tags}` : ''}`) }) @@ -196,6 +224,9 @@ After(async function (this: DifyWorld, { pickle, result }) { }) AfterAll(async () => { + const microphoneBrowser = await microphoneBrowserPromise?.catch(() => undefined) + await microphoneBrowser?.close() await browser?.close() + microphoneBrowserPromise = undefined browser = undefined }) diff --git a/e2e/features/support/world.ts b/e2e/features/support/world.ts index 689275c9ccf..2a9d85833a0 100644 --- a/e2e/features/support/world.ts +++ b/e2e/features/support/world.ts @@ -36,12 +36,18 @@ export type AgentV2WorkflowOutputVariable = { name: string type: string } +export type AgentBuilderSpeechToTextRequest = { + contentType: string + path: string + status: number +} export const createAgentBuilderWorldState = () => ({ preflight: { agentDecisionModel: undefined as AgentBuilderChatModel | undefined, brokenModel: undefined as AgentBuilderChatModel | undefined, preseededResources: {} as Record, + speechToTextModel: undefined as AgentBuilderChatModel | undefined, stableModel: undefined as AgentBuilderChatModel | undefined, }, accessPoint: { @@ -57,6 +63,9 @@ export const createAgentBuilderWorldState = () => ({ configure: { concurrentPage: undefined as Page | undefined, }, + speechToText: { + request: undefined as AgentBuilderSpeechToTextRequest | undefined, + }, workflow: { agentConsolePage: undefined as Page | undefined, outputVariables: [] as AgentV2WorkflowOutputVariable[], diff --git a/e2e/fixtures/test-materials/voice-input.wav b/e2e/fixtures/test-materials/voice-input.wav new file mode 100644 index 0000000000000000000000000000000000000000..128e73182fd88bccec1ed3a759d7cada2dd6cdc2 GIT binary patch literal 256078 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 z1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14c zNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-L zfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@ z1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14c zNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-L zfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@ z1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14c zNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-L zfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@ z1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14c zNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-L zfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@1W14cNPq-LfCNZ@ S1W14cNPq-LfCT path.join(testMaterialsDir, fileName) +export const getVoiceInputTestMaterialPath = () => getTestMaterialPath(voiceInputTestMaterial.fileName) export async function getGeneratedTextMaterialPath({ fileName,