mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 18:58:35 +08:00
Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: zyssyz123 <916125788@qq.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: 林玮 (Jade Lin) <linw1995@icloud.com> Co-authored-by: 盐粒 Yanli <mail@yanli.one>
356 lines
13 KiB
TypeScript
356 lines
13 KiB
TypeScript
import type { AgentSoulDifyToolConfig } from '@dify/contracts/api/console/agent/types.gen'
|
|
import type { DifyWorld } from '../../support/world'
|
|
import { Given, Then, When } from '@cucumber/cucumber'
|
|
import { expect } from '@playwright/test'
|
|
import { sendAgentServiceApiChatMessage } from '../../agent-v2/support/access-point'
|
|
import { createConfiguredTestAgent, getAgentComposerDraft } from '../../agent-v2/support/agent'
|
|
import { agentBuilderExpectedTokens, agentBuilderFixedInputs, agentBuilderPreseededResources } from '../../agent-v2/support/agent-builder-resources'
|
|
import { createAgentSoulConfigWithDifyTool, createAgentSoulConfigWithModel, normalAgentSoulConfig } from '../../agent-v2/support/agent-soul'
|
|
import { getPreseededOAuthToolConfig } from '../../agent-v2/support/preflight/agents'
|
|
import { asArray, asRecord, asString } from '../../agent-v2/support/preflight/common'
|
|
import { hasToolEntry } from '../../agent-v2/support/preflight/tools'
|
|
import { getPreseededToolContract } from '../../agent-v2/support/tools'
|
|
import { expectProviderToolActionVisible, getCurrentAgentId } from './configure-helpers'
|
|
|
|
const getToolsSection = (world: DifyWorld) =>
|
|
world.getPage().getByRole('region', { name: 'Tools' })
|
|
|
|
const getToolSelectorSearch = (world: DifyWorld) =>
|
|
world.getPage().getByRole('textbox', { name: 'Search integrations...' })
|
|
|
|
const jsonReplaceRuntimePrompt = [
|
|
'You are a Dify Agent E2E JSON tool verifier.',
|
|
'When the user asks to verify JSON Replace, call the JSON Replace tool exactly once.',
|
|
`Use content {"marker":"${agentBuilderExpectedTokens.jsonToolBefore}","nested":{"status":"keep"}}.`,
|
|
'Use query $.marker and replace_value E2E_AFTER.',
|
|
'After the tool returns, answer with the resulting JSON and include E2E_AFTER.',
|
|
].join(' ')
|
|
|
|
const expectJsonReplaceToolDraft = async (world: DifyWorld) => {
|
|
const agentId = getCurrentAgentId(world)
|
|
const tool = getPreseededToolContract(world, agentBuilderPreseededResources.jsonReplaceTool)
|
|
|
|
await expect.poll(
|
|
async () => {
|
|
const draft = await getAgentComposerDraft(agentId)
|
|
const tools = asArray(asRecord(draft.agent_soul?.tools).dify_tools)
|
|
|
|
return hasToolEntry(tools, tool)
|
|
},
|
|
{ timeout: 30_000 },
|
|
).toBe(true)
|
|
}
|
|
|
|
const getOAuth2ToolEntries = async (agentId: string) => {
|
|
const draft = await getAgentComposerDraft(agentId)
|
|
|
|
return asArray(asRecord(draft.agent_soul?.tools).dify_tools).filter((item) => {
|
|
const record = asRecord(item)
|
|
|
|
return record.credential_type === 'oauth2'
|
|
&& Boolean(asString(asRecord(record.credential_ref).id))
|
|
})
|
|
}
|
|
|
|
const getOAuth2ToolDisplayName = async (world: DifyWorld) => {
|
|
const [tool] = await getOAuth2ToolEntries(getCurrentAgentId(world))
|
|
const record = asRecord(tool)
|
|
const providerName = asString(record.provider) || asString(record.provider_id) || asString(record.plugin_id)
|
|
const toolName = asString(record.name) || asString(record.tool_name)
|
|
|
|
if (!providerName || !toolName)
|
|
throw new Error('Agent v2 OAuth2 tool fixture must include provider and tool names.')
|
|
|
|
return `${providerName} / ${toolName}`
|
|
}
|
|
|
|
const getPreseededOAuthToolAgent = (world: DifyWorld) => {
|
|
const resource = world.agentBuilder.preflight.preseededResources[
|
|
`${agentBuilderPreseededResources.oauthToolAgent} / OAuth2 tool credential`
|
|
]
|
|
if (!resource || resource.kind !== 'agent') {
|
|
throw new Error(
|
|
`Preseeded Agent "${agentBuilderPreseededResources.oauthToolAgent}" OAuth2 tool credential fixture is not available. Run the matching preflight step first.`,
|
|
)
|
|
}
|
|
|
|
return resource
|
|
}
|
|
|
|
const jsonReplaceToolConfig = (world: DifyWorld): AgentSoulDifyToolConfig => {
|
|
const tool = getPreseededToolContract(world, agentBuilderPreseededResources.jsonReplaceTool)
|
|
|
|
return {
|
|
credential_type: 'unauthorized',
|
|
enabled: true,
|
|
provider: tool.providerDisplayName,
|
|
provider_id: tool.providerName,
|
|
provider_type: 'builtin',
|
|
runtime_parameters: {
|
|
ensure_ascii: true,
|
|
replace_model: 'value',
|
|
value_decode: false,
|
|
},
|
|
tool_name: tool.toolName,
|
|
}
|
|
}
|
|
|
|
const getServiceApiSseEvents = (body: unknown) =>
|
|
asArray(asRecord(body).events).map(asRecord)
|
|
|
|
const getServiceApiEventData = (event: Record<string, unknown>) => asRecord(event.data)
|
|
|
|
const getServiceApiEventName = (event: Record<string, unknown>) => {
|
|
const data = getServiceApiEventData(event)
|
|
|
|
return asString(data.event) || asString(event.event)
|
|
}
|
|
|
|
const summarizeServiceApiRuntimeEvents = (body: unknown) =>
|
|
getServiceApiSseEvents(body).map((event, index) => {
|
|
const data = getServiceApiEventData(event)
|
|
const observation = asString(data.observation)
|
|
const answer = asString(data.answer)
|
|
|
|
return {
|
|
index,
|
|
event: getServiceApiEventName(event),
|
|
tool: asString(data.tool),
|
|
hasToolInput: Boolean(asString(data.tool_input)),
|
|
hasObservation: Boolean(observation),
|
|
observation: observation.slice(0, 180),
|
|
answer: answer.slice(0, 180),
|
|
}
|
|
})
|
|
|
|
const findJsonReplaceRuntimeThought = (body: unknown) =>
|
|
getServiceApiSseEvents(body).find((event) => {
|
|
const data = getServiceApiEventData(event)
|
|
const toolInput = asString(data.tool_input)
|
|
const observation = asString(data.observation)
|
|
|
|
return getServiceApiEventName(event) === 'agent_thought'
|
|
&& asString(data.tool) === 'json_replace'
|
|
&& toolInput.includes(agentBuilderExpectedTokens.jsonToolBefore)
|
|
&& toolInput.includes('$.marker')
|
|
&& observation.includes(agentBuilderExpectedTokens.jsonToolAfter)
|
|
})
|
|
|
|
const expectOAuth2CredentialPreserved = async (world: DifyWorld) => {
|
|
const preseededAgent = getPreseededOAuthToolAgent(world)
|
|
const expectedTool = await getPreseededOAuthToolConfig(preseededAgent.id)
|
|
const expected = asRecord(expectedTool)
|
|
const expectedCredentialRef = asRecord(expected.credential_ref)
|
|
const expectedCredentialId = asString(expectedCredentialRef.id)
|
|
const expectedProvider = asString(expected.provider_id) || asString(expected.provider) || asString(expected.plugin_id)
|
|
const expectedToolName = asString(expected.tool_name) || asString(expected.name)
|
|
|
|
await expect.poll(
|
|
async () => {
|
|
const tools = await getOAuth2ToolEntries(getCurrentAgentId(world))
|
|
const matchingTool = tools.find((item) => {
|
|
const record = asRecord(item)
|
|
const provider = asString(record.provider_id) || asString(record.provider) || asString(record.plugin_id)
|
|
const toolName = asString(record.tool_name) || asString(record.name)
|
|
|
|
return provider === expectedProvider && toolName === expectedToolName
|
|
})
|
|
const record = asRecord(matchingTool)
|
|
|
|
return {
|
|
credentialId: asString(asRecord(record.credential_ref).id),
|
|
credentialType: asString(record.credential_type),
|
|
}
|
|
},
|
|
{ timeout: 30_000 },
|
|
).toEqual({
|
|
credentialId: expectedCredentialId,
|
|
credentialType: 'oauth2',
|
|
})
|
|
}
|
|
|
|
Given(
|
|
'an Agent v2 test agent with the OAuth2 tool credential fixture has been created via API',
|
|
async function (this: DifyWorld) {
|
|
const preseededAgent = getPreseededOAuthToolAgent(this)
|
|
const oauthTool = await getPreseededOAuthToolConfig(preseededAgent.id)
|
|
const agent = await createConfiguredTestAgent({
|
|
agentSoul: createAgentSoulConfigWithDifyTool(normalAgentSoulConfig, oauthTool),
|
|
})
|
|
|
|
this.createdAgentIds.push(agent.id)
|
|
this.lastCreatedAgentName = agent.name
|
|
this.lastCreatedAgentRole = agent.role ?? undefined
|
|
},
|
|
)
|
|
|
|
Given(
|
|
'a runnable Agent v2 test agent with the JSON Replace tool has been created via API',
|
|
async function (this: DifyWorld) {
|
|
if (!this.agentBuilder.preflight.stableModel)
|
|
throw new Error('Create a JSON Replace runtime Agent after stable model preflight.')
|
|
|
|
const agent = await createConfiguredTestAgent({
|
|
agentSoul: createAgentSoulConfigWithDifyTool(
|
|
createAgentSoulConfigWithModel(
|
|
{
|
|
...normalAgentSoulConfig,
|
|
prompt: {
|
|
system_prompt: jsonReplaceRuntimePrompt,
|
|
},
|
|
},
|
|
this.agentBuilder.preflight.stableModel,
|
|
),
|
|
jsonReplaceToolConfig(this),
|
|
),
|
|
})
|
|
|
|
this.createdAgentIds.push(agent.id)
|
|
this.lastCreatedAgentName = agent.name
|
|
this.lastCreatedAgentRole = agent.role ?? undefined
|
|
},
|
|
)
|
|
|
|
When(
|
|
'I add the Agent Builder JSON Replace tool from the Tools selector',
|
|
async function (this: DifyWorld) {
|
|
const page = this.getPage()
|
|
const toolsSection = getToolsSection(this)
|
|
|
|
await expect(toolsSection).toBeVisible({ timeout: 30_000 })
|
|
await toolsSection.getByRole('button', { name: 'Add tool' }).click()
|
|
await page.getByRole('button', { name: /^Tool\b/ }).click()
|
|
|
|
const search = getToolSelectorSearch(this)
|
|
await expect(search).toBeVisible()
|
|
await search.fill('JSON Replace')
|
|
|
|
await page.getByRole('button', { exact: true, name: 'JSON Replace' }).click()
|
|
await expectProviderToolActionVisible(
|
|
toolsSection,
|
|
agentBuilderPreseededResources.jsonReplaceTool,
|
|
)
|
|
},
|
|
)
|
|
|
|
When(
|
|
'I search for the missing Agent v2 tool from the Tools selector',
|
|
async function (this: DifyWorld) {
|
|
const toolsSection = getToolsSection(this)
|
|
|
|
await expect(toolsSection).toBeVisible({ timeout: 30_000 })
|
|
await toolsSection.getByRole('button', { name: 'Add tool' }).click()
|
|
|
|
const search = getToolSelectorSearch(this)
|
|
await expect(search).toBeVisible()
|
|
await search.fill(agentBuilderFixedInputs.missingToolSearchWithSuffix)
|
|
},
|
|
)
|
|
|
|
When('I clear the Agent v2 tool selector search', async function (this: DifyWorld) {
|
|
const search = getToolSelectorSearch(this)
|
|
|
|
await search.fill('')
|
|
})
|
|
|
|
Then(
|
|
'the Agent v2 JSON Replace tool should be saved in the Agent v2 draft',
|
|
async function (this: DifyWorld) {
|
|
await expectJsonReplaceToolDraft(this)
|
|
},
|
|
)
|
|
|
|
Then(
|
|
'I should see the Agent v2 JSON Replace tool in the Tools section',
|
|
async function (this: DifyWorld) {
|
|
await expectProviderToolActionVisible(
|
|
getToolsSection(this),
|
|
agentBuilderPreseededResources.jsonReplaceTool,
|
|
)
|
|
await expectJsonReplaceToolDraft(this)
|
|
},
|
|
)
|
|
|
|
When('I send the Agent v2 Backend service API JSON Replace request', async function (this: DifyWorld) {
|
|
const serviceApiBaseURL = this.agentBuilder.accessPoint.serviceApiBaseURL
|
|
const apiKey = this.agentBuilder.accessPoint.generatedApiKey
|
|
if (!serviceApiBaseURL)
|
|
throw new Error('No Agent v2 service API endpoint found. Enable Backend service API first.')
|
|
if (!apiKey)
|
|
throw new Error('No Agent v2 API key found. Create a Backend service API key first.')
|
|
|
|
this.agentBuilder.accessPoint.serviceApiResponse = await sendAgentServiceApiChatMessage({
|
|
apiKey,
|
|
query: [
|
|
'Verify JSON Replace now.',
|
|
`Replace ${agentBuilderExpectedTokens.jsonToolBefore} with ${agentBuilderExpectedTokens.jsonToolAfter}.`,
|
|
`Return the JSON result and include ${agentBuilderExpectedTokens.jsonToolAfter}.`,
|
|
].join(' '),
|
|
serviceApiBaseURL,
|
|
})
|
|
})
|
|
|
|
Then(
|
|
'the Agent v2 Backend service API response should include the JSON Replace E2E marker',
|
|
async function (this: DifyWorld) {
|
|
const response = this.agentBuilder.accessPoint.serviceApiResponse
|
|
if (!response)
|
|
throw new Error('No Agent v2 Backend service API response was recorded.')
|
|
if (!response.ok)
|
|
throw new Error(`Agent v2 Backend service API JSON Replace request failed with ${response.status}: ${JSON.stringify(response.body)}`)
|
|
|
|
const jsonReplaceThought = findJsonReplaceRuntimeThought(response.body)
|
|
if (!jsonReplaceThought) {
|
|
throw new Error(
|
|
[
|
|
'Agent v2 Backend service API did not emit a JSON Replace agent_thought with matching tool input and observation.',
|
|
`Received events: ${JSON.stringify(summarizeServiceApiRuntimeEvents(response.body))}`,
|
|
].join('\n'),
|
|
)
|
|
}
|
|
|
|
const thought = getServiceApiEventData(jsonReplaceThought)
|
|
expect(asString(thought.tool_input)).toContain(agentBuilderExpectedTokens.jsonToolBefore)
|
|
expect(asString(thought.tool_input)).toContain('$.marker')
|
|
expect(asString(thought.observation)).toContain(agentBuilderExpectedTokens.jsonToolAfter)
|
|
expect(asString(asRecord(response.body).answer)).toContain(agentBuilderExpectedTokens.jsonToolAfter)
|
|
},
|
|
)
|
|
|
|
Then(
|
|
'I should see the Agent v2 OAuth2 tool authorized in the Tools section',
|
|
async function (this: DifyWorld) {
|
|
const toolsSection = getToolsSection(this)
|
|
const displayName = await getOAuth2ToolDisplayName(this)
|
|
|
|
await expectProviderToolActionVisible(toolsSection, displayName)
|
|
await expect(toolsSection.getByRole('button', { exact: true, name: 'Not authorized' }))
|
|
.not
|
|
.toBeVisible()
|
|
},
|
|
)
|
|
|
|
Then(
|
|
'the Agent v2 OAuth2 tool credential should remain saved in the Agent v2 draft',
|
|
async function (this: DifyWorld) {
|
|
await expectOAuth2CredentialPreserved(this)
|
|
},
|
|
)
|
|
|
|
Then('I should see the Agent v2 tool selector empty state', async function (this: DifyWorld) {
|
|
const page = this.getPage()
|
|
|
|
await expect(page.getByText('No integrations were found')).toBeVisible({ timeout: 30_000 })
|
|
await expect(page.getByRole('link', { name: 'Requests to the community' })).toBeVisible()
|
|
await expect(page.getByText(agentBuilderFixedInputs.missingToolSearchWithSuffix)).not.toBeVisible()
|
|
})
|
|
|
|
Then('I should see the Agent v2 tool selector ready for another search', async function (this: DifyWorld) {
|
|
const page = this.getPage()
|
|
const search = getToolSelectorSearch(this)
|
|
|
|
await expect(search).toHaveValue('')
|
|
await expect(page.getByText('No integrations were found')).not.toBeVisible()
|
|
await expect(page.getByText('All tools')).toBeVisible()
|
|
})
|