import type { DataTable } from '@cucumber/cucumber' import type { DeclaredOutputConfig } from '@dify/contracts/api/console/apps/types.gen' import type { AgentV2WorkflowOutputVariable, DifyWorld } from '../../support/world' import { Then, When } from '@cucumber/cucumber' import { expect } from '@playwright/test' import { getWorkflowDraft } from '../../../support/api' const agentV2WorkflowNodeId = 'agent-v2' const taskOutputName = 'e2e_report' const renamedTaskOutputName = 'e2e_final_report' const getAgentOutputToken = (name: string) => `[§output:${name}:${name}§]` const getCurrentAppId = (world: DifyWorld) => { const appId = world.createdAppIds.at(-1) if (!appId) throw new Error('No app ID found. Create a workflow app first.') return appId } const getAgentV2WorkflowNodeData = async (appId: string) => { const draft = await getWorkflowDraft(appId) const agentNode = draft.graph.nodes.find((node) => node.id === agentV2WorkflowNodeId) if (!agentNode) throw new Error( `Workflow draft ${appId} does not include Agent v2 node ${agentV2WorkflowNodeId}.`, ) return agentNode.data ?? {} } const getDeclaredOutputsFromDraft = async (appId: string): Promise => { const data = await getAgentV2WorkflowNodeData(appId) const outputs = data.agent_declared_outputs if (!Array.isArray(outputs)) return [] return outputs as DeclaredOutputConfig[] } const getOutputVariablesFromDraft = async (appId: string) => getDeclaredOutputsFromDraft(appId) const waitForWorkflowDraftSave = (world: DifyWorld, appId: string) => world .getPage() .waitForResponse( (response) => response.request().method() === 'POST' && new URL(response.url()).pathname.endsWith(`/console/api/apps/${appId}/workflows/draft`), ) const openWorkflowOutputVariablesPanel = async (world: DifyWorld) => { const page = world.getPage() const outputVariablesButton = page.getByRole('button', { name: 'Output Variables' }) const newOutputButton = page.getByRole('button', { name: 'New output' }) await expect(outputVariablesButton).toHaveAttribute('aria-expanded', 'false') await outputVariablesButton.click() await expect(outputVariablesButton).toHaveAttribute('aria-expanded', 'true') await expect(newOutputButton).toBeVisible() } const fillOutputVariableEditor = async ( world: DifyWorld, { name, required = false, type = 'string', }: { name: string required?: boolean type?: string }, ) => { const page = world.getPage() const editor = page.getByRole('form', { name: 'Output variable editor' }) await expect(editor).toBeVisible() await editor.getByRole('textbox', { name: 'Field name' }).fill(name) if (type !== 'string') { await editor.getByLabel('Output type').click() await page.getByRole('option', { name: type, exact: true }).click() } if (required) await editor.getByRole('switch', { name: 'Required' }).click() } When( 'I insert an output reference from the Agent v2 workflow node task editor', async function (this: DifyWorld) { const page = this.getPage() const appId = getCurrentAppId(this) const taskEditor = page.getByRole('textbox', { name: 'Agent task' }) await expect(taskEditor).toBeVisible() await taskEditor.click() await page.keyboard.type('/') const insertResponse = waitForWorkflowDraftSave(this, appId) await page.getByRole('button', { name: 'New output' }).click() expect((await insertResponse).ok()).toBe(true) const nameInput = page.getByRole('textbox', { name: 'Field name' }) await expect(nameInput).toBeVisible() await nameInput.fill(taskOutputName) const renameResponse = waitForWorkflowDraftSave(this, appId) await nameInput.press('Enter') expect((await renameResponse).ok()).toBe(true) }, ) When('I rename the Agent v2 workflow node task output reference', async function (this: DifyWorld) { const page = this.getPage() const appId = getCurrentAppId(this) const taskEditor = page.getByRole('textbox', { name: 'Agent task' }) await taskEditor.getByRole('button', { name: `Edit ${taskOutputName}`, exact: true }).click() const editor = page.getByRole('form', { name: 'Output variable editor' }) await expect(editor).toBeVisible() await editor.getByRole('textbox', { name: 'Field name' }).fill(renamedTaskOutputName) const saveResponse = waitForWorkflowDraftSave(this, appId) await editor.getByRole('button', { name: 'Confirm' }).click() expect((await saveResponse).ok()).toBe(true) await expect(editor).not.toBeVisible() }) When( 'I add these Agent v2 workflow node output variables', async function (this: DifyWorld, table: DataTable) { const page = this.getPage() const appId = getCurrentAppId(this) const rows = table.hashes() as AgentV2WorkflowOutputVariable[] this.agentBuilder.workflow.outputVariables = rows await openWorkflowOutputVariablesPanel(this) for (const row of rows) { await page.getByRole('button', { name: 'New output' }).click() await fillOutputVariableEditor(this, row) const editor = page.getByRole('form', { name: 'Output variable editor' }) const saveResponse = waitForWorkflowDraftSave(this, appId) await editor.getByRole('button', { name: 'Confirm' }).click() expect((await saveResponse).ok()).toBe(true) await expect(editor).not.toBeVisible() } }, ) When( 'I add a required Agent v2 workflow node object output variable with text and analysis fields', async function (this: DifyWorld) { const page = this.getPage() const appId = getCurrentAppId(this) await openWorkflowOutputVariablesPanel(this) await page.getByRole('button', { name: 'New output' }).click() await fillOutputVariableEditor(this, { name: 'response', required: true, type: 'object', }) let saveResponse = waitForWorkflowDraftSave(this, appId) await page .getByRole('form', { name: 'Output variable editor' }) .getByRole('button', { name: 'Confirm' }) .click() expect((await saveResponse).ok()).toBe(true) for (const fieldName of ['text', 'analysis']) { await page.getByText('response', { exact: true }).hover() await page.getByRole('button', { name: 'Add response' }).click() await fillOutputVariableEditor(this, { name: fieldName }) saveResponse = waitForWorkflowDraftSave(this, appId) await page .getByRole('form', { name: 'Output variable editor' }) .getByRole('button', { name: 'Confirm' }) .click() expect((await saveResponse).ok()).toBe(true) } }, ) Then( 'the Agent v2 workflow node output variables should be saved in the workflow draft', async function (this: DifyWorld) { const appId = getCurrentAppId(this) const expectedOutputVariables = this.agentBuilder.workflow.outputVariables if (expectedOutputVariables.length === 0) throw new Error('No Agent v2 workflow output variables were recorded for this scenario.') await expect .poll( async () => { const outputs = await getOutputVariablesFromDraft(appId) return expectedOutputVariables.map((expected) => { const output = outputs.find((item) => item.name === expected.name) return { name: output?.name, type: output?.type === 'array' ? `array[${output.array_item?.type ?? 'object'}]` : output?.type, } }) }, { timeout: 30_000, }, ) .toEqual(expectedOutputVariables) }, ) Then('I should see the Agent v2 workflow node output variables', async function (this: DifyWorld) { const page = this.getPage() const expectedOutputVariables = this.agentBuilder.workflow.outputVariables if (expectedOutputVariables.length === 0) throw new Error('No Agent v2 workflow output variables were recorded for this scenario.') await openWorkflowOutputVariablesPanel(this) for (const output of expectedOutputVariables) { await expect(page.getByText(output.name, { exact: true })).toBeVisible() } }) Then( 'the Agent v2 workflow node nested object output variable should be saved in the workflow draft', async function (this: DifyWorld) { const appId = getCurrentAppId(this) await expect .poll( async () => { const outputs = await getDeclaredOutputsFromDraft(appId) const response = outputs.find((output) => output.name === 'response') return { children: response?.children?.map((child) => ({ name: child.name, required: child.required, type: child.type, })), name: response?.name, required: response?.required, type: response?.type, } }, { timeout: 30_000, }, ) .toEqual({ children: [ { name: 'text', required: false, type: 'string', }, { name: 'analysis', required: false, type: 'string', }, ], name: 'response', required: true, type: 'object', }) }, ) Then( 'the Agent v2 workflow node task should reference the output', async function (this: DifyWorld) { await expectAgentTaskOutputReference(this, taskOutputName) }, ) Then( 'the Agent v2 workflow node task should reference the renamed output', async function (this: DifyWorld) { await expectAgentTaskOutputReference(this, renamedTaskOutputName, taskOutputName) }, ) Then( 'I should see the Agent v2 workflow node nested object output variable', async function (this: DifyWorld) { const page = this.getPage() await openWorkflowOutputVariablesPanel(this) await expect(page.getByText('response', { exact: true })).toBeVisible() await expect(page.getByText('Required', { exact: true })).toBeVisible() await expect(page.getByText('analysis', { exact: true })).toBeVisible() }, ) async function expectAgentTaskOutputReference( world: DifyWorld, expectedName: string, unexpectedName?: string, ) { const page = world.getPage() const appId = getCurrentAppId(world) const taskEditor = page.getByRole('textbox', { name: 'Agent task' }) await expect .poll( async () => { const data = await getAgentV2WorkflowNodeData(appId) const outputs = Array.isArray(data.agent_declared_outputs) ? (data.agent_declared_outputs as DeclaredOutputConfig[]) : [] const expectedOutput = outputs.find((output) => output.name === expectedName) return { agentTask: data.agent_task, expectedOutput: expectedOutput ? { name: expectedOutput.name, type: expectedOutput.type, } : undefined, unexpectedOutput: unexpectedName ? outputs.some((output) => output.name === unexpectedName) : false, } }, { timeout: 30_000 }, ) .toEqual({ agentTask: expect.stringContaining(getAgentOutputToken(expectedName)), expectedOutput: { name: expectedName, type: 'string', }, unexpectedOutput: false, }) await expect( taskEditor.getByRole('button', { name: `Edit ${expectedName}`, exact: true }), ).toBeVisible() if (unexpectedName) await expect( taskEditor.getByRole('button', { name: `Edit ${unexpectedName}`, exact: true }), ).toHaveCount(0) }