dify/e2e/features/step-definitions/agent-v2/configure.steps.ts

461 lines
16 KiB
TypeScript

import type { Page } from '@playwright/test'
import type { DifyWorld } from '../../support/world'
import { Given, Then, When } from '@cucumber/cucumber'
import { expect } from '@playwright/test'
import { waitForAgentConfigureAutosaved } from '../../../support/agent-configure'
import {
createConfiguredTestAgent,
createTestAgent,
getAgentComposerDraft,
getAgentConfigurePath,
saveAgentComposerDraft,
} from '../../agent-v2/support/agent'
import { getAgentDriveSkills, uploadAgentDriveSkill } from '../../agent-v2/support/agent-drive'
import {
concurrentFirstAgentPrompt,
concurrentSecondAgentPrompt,
createAgentSoulConfigWithModel,
createPublishableAgentSoulConfig,
normalAgentPrompt,
normalAgentSoulConfig,
updatedAgentPrompt,
} from '../../agent-v2/support/agent-soul'
import {
agentBuilderTestMaterials,
getAgentBuilderTestMaterialPath,
} from '../../agent-v2/support/test-materials'
import {
expectNormalAgentPromptDraft,
getCurrentAgentId,
getPreseededAgent,
} from './configure-helpers'
const concurrentAgentPrompts = [concurrentFirstAgentPrompt, concurrentSecondAgentPrompt]
const getPromptEditor = (page: Page) =>
page.getByRole('region', { name: 'Prompt' }).getByRole('textbox', { name: 'Prompt' })
async function fillAgentPromptEditor(page: Page, prompt: string) {
const promptSection = page.getByRole('region', { name: 'Prompt' })
await expect(promptSection).toBeVisible({ timeout: 30_000 })
await getPromptEditor(page).fill(prompt)
}
async function selectAgentModel(page: Page, modelName: string) {
await page.getByRole('combobox').first().click()
await page.getByPlaceholder('Search model').fill(modelName)
const escapedModelName = modelName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
await page.getByRole('option', { name: new RegExp(`${escapedModelName}(?:\\s|$)`) }).click()
}
async function expectAgentComposerPrompt(agentId: string, prompt: string) {
await expect
.poll(async () => (await getAgentComposerDraft(agentId)).agent_soul?.prompt?.system_prompt, {
timeout: 30_000,
})
.toBe(prompt)
}
Given('an Agent v2 test agent has been created via API', async function (this: DifyWorld) {
const agent = await createTestAgent()
this.createdAgentIds.push(agent.id)
this.lastCreatedAgentName = agent.name
this.lastCreatedAgentRole = agent.role ?? undefined
})
Given(
'a basic configured Agent v2 test agent has been created via API',
async function (this: DifyWorld) {
const agent = await createConfiguredTestAgent()
this.createdAgentIds.push(agent.id)
this.lastCreatedAgentName = agent.name
this.lastCreatedAgentRole = agent.role ?? undefined
},
)
Given('a runnable Agent v2 test agent has been created via API', async function (this: DifyWorld) {
if (!this.agentBuilder.fixtures.stableModel)
throw new Error('Create a runnable Agent v2 test agent after stable model fixture setup.')
const agent = await createConfiguredTestAgent({
agentSoul: createAgentSoulConfigWithModel(
normalAgentSoulConfig,
this.agentBuilder.fixtures.stableModel,
),
})
this.createdAgentIds.push(agent.id)
this.lastCreatedAgentName = agent.name
this.lastCreatedAgentRole = agent.role ?? undefined
})
Given(
'a runnable Agent v2 test agent using the agent-decision model has been created via API',
async function (this: DifyWorld) {
if (!this.agentBuilder.fixtures.agentDecisionModel) {
throw new Error(
'Create an agent-decision Agent v2 test agent after agent-decision model fixture setup.',
)
}
const agent = await createConfiguredTestAgent({
agentSoul: createAgentSoulConfigWithModel(
normalAgentSoulConfig,
this.agentBuilder.fixtures.agentDecisionModel,
),
})
this.createdAgentIds.push(agent.id)
this.lastCreatedAgentName = agent.name
this.lastCreatedAgentRole = agent.role ?? undefined
},
)
Given('a minimal Agent v2 composer draft has been synced', async function (this: DifyWorld) {
const agentId = getCurrentAgentId(this)
await saveAgentComposerDraft(agentId)
})
Given('the Agent v2 composer draft uses the normal E2E prompt', async function (this: DifyWorld) {
await saveAgentComposerDraft(getCurrentAgentId(this), normalAgentSoulConfig)
})
Given('the Agent v2 composer draft is publishable', async function (this: DifyWorld) {
await saveAgentComposerDraft(
getCurrentAgentId(this),
createPublishableAgentSoulConfig(normalAgentSoulConfig),
)
})
Given(
'the e2e-summary-skill Skill is available to the Agent v2 test agent',
async function (this: DifyWorld) {
const agentId = getCurrentAgentId(this)
const upload = await uploadAgentDriveSkill({
agentId,
fileName: agentBuilderTestMaterials.summarySkill,
filePath: getAgentBuilderTestMaterialPath('summarySkill'),
})
this.createdAgentDriveFiles.push({ agentId, key: upload.skill.skill_md_key })
if (upload.skill.archive_key)
this.createdAgentDriveFiles.push({ agentId, key: upload.skill.archive_key })
},
)
Then(
'the Agent v2 test agent should include drive skill {string}',
async function (this: DifyWorld, skillName: string) {
const skills = await getAgentDriveSkills(getCurrentAgentId(this))
expect(skills.map((skill) => skill.name)).toContain(skillName)
},
)
When('I open the Agent v2 configure page', async function (this: DifyWorld) {
await this.getPage().goto(getAgentConfigurePath(getCurrentAgentId(this)))
})
When(
'I select the stable E2E model in the Agent v2 model selector',
async function (this: DifyWorld) {
const stableModel = this.agentBuilder.fixtures.stableModel
if (!stableModel)
throw new Error('Stable chat model fixture setup must run before selecting the Agent model.')
await selectAgentModel(this.getPage(), stableModel.name)
},
)
When(
'I select the agent-decision E2E model in the Agent v2 model selector',
async function (this: DifyWorld) {
const model = this.agentBuilder.fixtures.agentDecisionModel
if (!model)
throw new Error(
'Agent-decision chat model fixture setup must run before selecting the Agent model.',
)
await selectAgentModel(this.getPage(), model.name)
},
)
When('I switch to the Agent v2 Configure section', async function (this: DifyWorld) {
const page = this.getPage()
const agentId = getCurrentAgentId(this)
await page.getByRole('link', { name: 'Configure' }).click()
await expect(page).toHaveURL(new RegExp(`/agents/${agentId}/configure(?:\\?.*)?$`))
await expect(page.getByRole('heading', { name: 'Configure' })).toBeVisible({ timeout: 30_000 })
})
When(
'I leave the Agent v2 configure page immediately after editing',
async function (this: DifyWorld) {
const page = this.getPage()
await page.goto('/agents')
await expect(page).toHaveURL(/\/agents(?:\?.*)?$/)
},
)
When('I open the Agent v2 configure page from the Agent Roster', async function (this: DifyWorld) {
const page = this.getPage()
const agentId = getCurrentAgentId(this)
const agentName = this.lastCreatedAgentName
if (!agentName) throw new Error('No Agent v2 name found. Create an Agent v2 test agent first.')
await page.goto('/agents')
await page.getByRole('link', { name: agentName }).click()
await expect(page).toHaveURL(new RegExp(`/agents/${agentId}/configure(?:\\?.*)?$`))
await expect(page.getByRole('heading', { name: 'Configure' })).toBeVisible({ timeout: 30_000 })
})
When(
'I open the preseeded Agent v2 configure page for {string} from the Agent Roster',
async function (this: DifyWorld, agentName: string) {
const page = this.getPage()
const agent = getPreseededAgent(this, agentName)
await page.goto('/agents')
await page.getByRole('link', { name: agentName }).click()
await expect(page).toHaveURL(new RegExp(`/agents/${agent.id}/configure(?:\\?.*)?$`))
await expect(page.getByRole('heading', { name: 'Configure' })).toBeVisible({ timeout: 30_000 })
},
)
When(
'I fill the Agent v2 prompt editor with the normal E2E prompt',
async function (this: DifyWorld) {
await fillAgentPromptEditor(this.getPage(), normalAgentPrompt)
},
)
When(
'I fill the Agent v2 prompt editor with the updated E2E prompt',
async function (this: DifyWorld) {
await fillAgentPromptEditor(this.getPage(), updatedAgentPrompt)
},
)
When('I open the same Agent v2 configure page in another tab', async function (this: DifyWorld) {
if (!this.context)
throw new Error('Playwright context has not been initialized for this scenario.')
const agentId = getCurrentAgentId(this)
const concurrentPage = await this.context.newPage()
this.agentBuilder.configure.concurrentPage = concurrentPage
await concurrentPage.goto(getAgentConfigurePath(agentId))
await expect(concurrentPage).toHaveURL(new RegExp(`/agents/${agentId}/configure(?:\\?.*)?$`))
await expect(concurrentPage.getByRole('heading', { name: 'Configure' })).toBeVisible({
timeout: 30_000,
})
})
When('I save the Agent v2 prompt from the first configure tab', async function (this: DifyWorld) {
const agentId = getCurrentAgentId(this)
await fillAgentPromptEditor(this.getPage(), concurrentFirstAgentPrompt)
await waitForAgentConfigureAutosaved(this.getPage())
await expectAgentComposerPrompt(agentId, concurrentFirstAgentPrompt)
})
When('I save the Agent v2 prompt from the second configure tab', async function (this: DifyWorld) {
const agentId = getCurrentAgentId(this)
const concurrentPage = this.agentBuilder.configure.concurrentPage
if (!concurrentPage)
throw new Error('Open the same Agent v2 configure page in another tab before editing it.')
await fillAgentPromptEditor(concurrentPage, concurrentSecondAgentPrompt)
await waitForAgentConfigureAutosaved(concurrentPage)
await expectAgentComposerPrompt(agentId, concurrentSecondAgentPrompt)
})
When('I refresh both Agent v2 configure tabs', async function (this: DifyWorld) {
const page = this.getPage()
const concurrentPage = this.agentBuilder.configure.concurrentPage
if (!concurrentPage)
throw new Error('Open the same Agent v2 configure page in another tab before refreshing it.')
await Promise.all([page.reload(), concurrentPage.reload()])
await expect(page.getByRole('heading', { name: 'Configure' })).toBeVisible({ timeout: 30_000 })
await expect(concurrentPage.getByRole('heading', { name: 'Configure' })).toBeVisible({
timeout: 30_000,
})
})
Then('I should be on the Agent v2 configure page', async function (this: DifyWorld) {
const agentId = getCurrentAgentId(this)
await expect(this.getPage()).toHaveURL(new RegExp(`/agents/${agentId}/configure(?:\\?.*)?$`))
})
Then('I should see the Agent v2 configure workspace', async function (this: DifyWorld) {
const page = this.getPage()
const agentName = this.lastCreatedAgentName
if (!agentName) throw new Error('No Agent v2 name found. Create an Agent v2 test agent first.')
await expect(page.getByRole('region', { name: 'Configure' })).toBeVisible({ timeout: 30_000 })
await expect(page.getByRole('heading', { name: 'Configure' })).toBeVisible()
await expect(page.getByText(agentName, { exact: true })).toBeVisible()
})
Then(
'I should see the normal E2E prompt in the Agent v2 prompt editor',
async function (this: DifyWorld) {
const page = this.getPage()
await expect(getPromptEditor(page)).toContainText(normalAgentPrompt, { timeout: 30_000 })
},
)
Then(
'I should see the stable E2E model in the Agent v2 model selector',
async function (this: DifyWorld) {
const stableModel = this.agentBuilder.fixtures.stableModel
if (!stableModel)
throw new Error('Stable chat model fixture setup must run before asserting the Agent model.')
await expect(this.getPage().getByText(stableModel.name, { exact: true })).toBeVisible({
timeout: 30_000,
})
},
)
Then(
'I should see the agent-decision E2E model in the Agent v2 model selector',
async function (this: DifyWorld) {
const model = this.agentBuilder.fixtures.agentDecisionModel
if (!model)
throw new Error(
'Agent-decision chat model fixture setup must run before asserting the Agent model.',
)
await expect(this.getPage().getByText(model.name, { exact: true })).toBeVisible({
timeout: 30_000,
})
},
)
Then(
'I should see the updated E2E prompt in the Agent v2 prompt editor',
async function (this: DifyWorld) {
const page = this.getPage()
await expect(getPromptEditor(page)).toContainText(updatedAgentPrompt, { timeout: 30_000 })
},
)
Then(
'both Agent v2 configure tabs and the Agent v2 draft should show one saved concurrent prompt',
async function (this: DifyWorld) {
const agentId = getCurrentAgentId(this)
const concurrentPage = this.agentBuilder.configure.concurrentPage
if (!concurrentPage)
throw new Error(
'Open the same Agent v2 configure page in another tab before asserting convergence.',
)
let savedPrompt = ''
await expect
.poll(
async () => {
const prompt = (await getAgentComposerDraft(agentId)).agent_soul?.prompt?.system_prompt
if (prompt && concurrentAgentPrompts.includes(prompt)) savedPrompt = prompt
return !!savedPrompt
},
{ timeout: 30_000 },
)
.toBe(true)
await expect(getPromptEditor(this.getPage())).toContainText(savedPrompt)
await expect(getPromptEditor(concurrentPage)).toContainText(savedPrompt)
},
)
Then(
'the normal Agent v2 draft should still use the normal E2E prompt',
async function (this: DifyWorld) {
await expectNormalAgentPromptDraft(this)
},
)
Then(
'the normal Agent v2 draft should use the normal E2E prompt',
async function (this: DifyWorld) {
await expectNormalAgentPromptDraft(this)
},
)
Then(
'the normal Agent v2 draft should use the updated E2E prompt',
async function (this: DifyWorld) {
await expect
.poll(async () => (await getAgentComposerDraft(getCurrentAgentId(this))).agent_soul?.prompt, {
timeout: 30_000,
})
.toEqual({ system_prompt: updatedAgentPrompt })
},
)
Then('the Agent v2 draft should use the stable E2E model', async function (this: DifyWorld) {
const stableModel = this.agentBuilder.fixtures.stableModel
if (!stableModel)
throw new Error('Stable chat model fixture setup must run before asserting the Agent model.')
await expect
.poll(
async () => {
const model = (await getAgentComposerDraft(getCurrentAgentId(this))).agent_soul?.model
const modelConfig =
typeof model === 'object' && model !== null && !Array.isArray(model)
? (model as Record<string, unknown>)
: undefined
return {
model: modelConfig?.model,
provider: modelConfig?.model_provider,
}
},
{ timeout: 30_000 },
)
.toEqual({
model: stableModel.name,
provider: stableModel.provider,
})
})
Then(
'the Agent v2 draft should use the agent-decision E2E model',
async function (this: DifyWorld) {
const model = this.agentBuilder.fixtures.agentDecisionModel
if (!model)
throw new Error(
'Agent-decision chat model fixture setup must run before asserting the Agent model.',
)
await expect
.poll(
async () => {
const draftModel = (await getAgentComposerDraft(getCurrentAgentId(this))).agent_soul
?.model
const modelConfig =
typeof draftModel === 'object' && draftModel !== null && !Array.isArray(draftModel)
? (draftModel as Record<string, unknown>)
: undefined
return {
model: modelConfig?.model,
provider: modelConfig?.model_provider,
}
},
{ timeout: 30_000 },
)
.toEqual({
model: model.name,
provider: model.provider,
})
},
)