Compare commits

..

1 Commits

Author SHA1 Message Date
cacaa5eb10 fix(api): scope segment batch deletes 2026-06-30 18:05:58 +08:00
12 changed files with 30 additions and 405 deletions

View File

@ -3872,7 +3872,14 @@ class SegmentService:
session.add(document)
# Delete database records
session.execute(delete(DocumentSegment).where(DocumentSegment.id.in_(segment_ids)))
session.execute(
delete(DocumentSegment).where(
DocumentSegment.id.in_(segment_db_ids),
DocumentSegment.dataset_id == dataset.id,
DocumentSegment.document_id == document.id,
DocumentSegment.tenant_id == current_user.current_tenant_id,
)
)
session.commit()
@classmethod

View File

@ -821,7 +821,9 @@ class TestSegmentServiceMutations:
# scalars() for child_node_ids
mock_db.session.scalars.return_value.all.return_value = ["child-1"]
SegmentService.delete_segments(["segment-1", "segment-2"], document, dataset, mock_db.session)
SegmentService.delete_segments(
["segment-1", "segment-2", "foreign-segment"], document, dataset, mock_db.session
)
assert document.word_count == 0
mock_db.session.add.assert_called_once_with(document)
@ -832,6 +834,13 @@ class TestSegmentServiceMutations:
["segment-1", "segment-2"],
["child-1"],
)
delete_stmt = mock_db.session.execute.call_args_list[1].args[0]
delete_sql = str(delete_stmt.compile(compile_kwargs={"literal_binds": True}))
assert "document_segments.id IN ('segment-1', 'segment-2')" in delete_sql
assert "document_segments.dataset_id = 'dataset-1'" in delete_sql
assert "document_segments.document_id = 'doc-1'" in delete_sql
assert "document_segments.tenant_id = 'tenant-1'" in delete_sql
assert "foreign-segment" not in delete_sql
mock_db.session.commit.assert_called_once()
def test_update_segments_status_enables_only_segments_without_indexing_cache(self):

View File

@ -1,26 +0,0 @@
@agent-v2 @authenticated @access-point @core
Feature: Agent v2 Access Point
Scenario: Access Point shows the available Agent v2 access surfaces
Given I am signed in as the default E2E admin
And an Agent v2 test agent has been created via API
When I open the Agent v2 Access Point page
Then I should see the Agent v2 Access Point overview
Scenario: Backend service API supports endpoint copy, key creation, and API reference navigation
Given I am signed in as the default E2E admin
And an Agent v2 test agent has been created via API
And Agent v2 Backend service API access has been enabled via API
When I open the Agent v2 configure page from the Agent Roster
And I switch to the Agent v2 Access Point section
Then I should see the Agent v2 Backend service API endpoint
When I copy the Agent v2 Backend service API endpoint
Then the Agent v2 Backend service API endpoint should show it was copied
When I open Agent v2 API key management
Then Agent v2 API keys should not expose a secret by default
When I create a new Agent v2 API key
Then I should see the newly generated Agent v2 API key once
When I close the newly generated Agent v2 API key
Then the Agent v2 API key list should not expose the full generated secret
When I close Agent v2 API key management
And I open the Agent v2 API Reference
Then the Agent v2 API Reference should open in a new tab

View File

@ -1,16 +0,0 @@
@agent-v2 @authenticated @build @core
Feature: Agent v2 build draft
Scenario: Discarding a Build draft keeps the original Agent configuration
Given I am signed in as the default E2E admin
And an Agent v2 test agent has been created via API
And the Agent v2 composer draft uses the normal E2E prompt
And an Agent v2 Build draft uses the updated E2E prompt
When I open the Agent v2 configure page
Then I should see the Agent v2 Build draft pending changes
And I should see the updated E2E prompt in the Agent v2 prompt editor
When I discard the Agent v2 Build draft
Then I should see the normal E2E prompt in the Agent v2 prompt editor
And the Agent v2 Build draft should no longer be active
When I refresh the current page
Then I should see the normal E2E prompt in the Agent v2 prompt editor
And the Agent v2 Build draft should no longer be active

View File

@ -1,10 +0,0 @@
@agent-v2 @authenticated @core
Feature: Agent v2 configure persistence
Scenario: Persisted Agent v2 instructions remain visible after refresh
Given I am signed in as the default E2E admin
And an Agent v2 test agent has been created via API
And the Agent v2 composer draft uses the normal E2E prompt
When I open the Agent v2 configure page
Then I should see the normal E2E prompt in the Agent v2 prompt editor
When I refresh the current page
Then I should see the normal E2E prompt in the Agent v2 prompt editor

View File

@ -1,9 +0,0 @@
@agent-v2 @authenticated @core
Feature: Agent v2 configure validation
Scenario: Preview is unavailable until a required model is configured
Given I am signed in as the default E2E admin
And an Agent v2 test agent has been created via API
And the Agent v2 composer draft uses the normal E2E prompt
When I open the Agent v2 configure page
Then Agent v2 Preview should be unavailable until a model is configured
And I should see the normal E2E prompt in the Agent v2 prompt editor

View File

@ -1,9 +0,0 @@
@agent-v2 @authenticated @publish @core
Feature: Agent v2 publish
Scenario: Publish a configured Agent v2 draft
Given I am signed in as the default E2E admin
And an Agent v2 test agent has been created via API
And the Agent v2 composer draft uses the normal E2E prompt
When I open the Agent v2 configure page
And I publish the Agent v2 draft
Then the Agent v2 draft should be published and up to date

View File

@ -1,175 +0,0 @@
import type { DifyWorld } from '../../support/world'
import { Given, Then, When } from '@cucumber/cucumber'
import { expect } from '@playwright/test'
import { getAgentAccessPath, setAgentApiAccess } from '../../../support/agent'
const getCurrentAgentId = (world: DifyWorld) => {
const agentId = world.createdAgentIds.at(-1)
if (!agentId)
throw new Error('No Agent v2 ID found. Create an Agent v2 test agent first.')
return agentId
}
Given(
'Agent v2 Backend service API access has been enabled via API',
async function (this: DifyWorld) {
const apiAccess = await setAgentApiAccess(getCurrentAgentId(this), true)
this.lastAgentServiceApiBaseURL = apiAccess.service_api_base_url
},
)
When('I open the Agent v2 Access Point page', async function (this: DifyWorld) {
await this.getPage().goto(getAgentAccessPath(getCurrentAgentId(this)))
})
When('I switch to the Agent v2 Access Point section', async function (this: DifyWorld) {
const page = this.getPage()
const agentId = getCurrentAgentId(this)
await page.getByRole('link', { name: 'Access Point' }).click()
await expect(page).toHaveURL(new RegExp(`/roster/agent/${agentId}/access(?:\\?.*)?$`))
await expect(page.getByRole('region', { name: 'Access Point' })).toBeVisible()
})
Then('I should see the Agent v2 Access Point overview', async function (this: DifyWorld) {
const page = this.getPage()
const accessRegion = page.getByRole('region', { name: 'Access Point' })
await expect(accessRegion).toBeVisible({ timeout: 30_000 })
await expect(accessRegion.getByRole('heading', { name: 'Access Point' })).toBeVisible()
await expect(accessRegion.getByRole('heading', { name: 'Web app' })).toBeVisible()
await expect(accessRegion.getByRole('heading', { name: 'Backend service API' })).toBeVisible()
await expect(accessRegion.getByRole('heading', { name: 'Workflow access' })).toBeVisible()
await expect(accessRegion.getByRole('columnheader', { name: 'Name' })).toBeVisible()
await expect(accessRegion.getByRole('columnheader', { name: 'Version' })).toBeVisible()
await expect(accessRegion.getByRole('columnheader', { name: 'Nodes' })).toBeVisible()
await expect(accessRegion.getByRole('columnheader', { name: 'Last updated' })).toBeVisible()
await expect(accessRegion.getByRole('columnheader', { name: 'Actions' })).toBeVisible()
await expect(accessRegion.getByText('No workflow references yet.')).toBeVisible()
})
Then('I should see the Agent v2 Backend service API endpoint', async function (this: DifyWorld) {
const page = this.getPage()
if (!this.lastAgentServiceApiBaseURL)
throw new Error('No Agent v2 service API endpoint found. Enable Backend service API first.')
await expect(page.getByRole('heading', { name: 'Backend service API' })).toBeVisible({
timeout: 30_000,
})
await expect(page.getByText('Service API Endpoint')).toBeVisible()
await expect(page.getByText(this.lastAgentServiceApiBaseURL)).toBeVisible()
await expect(page.getByLabel('Copy service API endpoint')).toBeEnabled()
})
When('I copy the Agent v2 Backend service API endpoint', async function (this: DifyWorld) {
await this.getPage().getByLabel('Copy service API endpoint').click()
})
Then(
'the Agent v2 Backend service API endpoint should show it was copied',
async function (this: DifyWorld) {
await expect(this.getPage().getByLabel('Copied')).toBeVisible()
},
)
When('I open Agent v2 API key management', async function (this: DifyWorld) {
await this.getPage()
.getByRole('button', { name: /^API Key\b/ })
.click()
})
Then('Agent v2 API keys should not expose a secret by default', async function (this: DifyWorld) {
const page = this.getPage()
const dialog = page.getByRole('dialog', { name: /API Secret key/i })
await expect(dialog).toBeVisible()
await expect(dialog.getByText('Secret Key', { exact: true })).toBeVisible()
await expect(dialog.getByText('CREATED', { exact: true })).toBeVisible()
await expect(dialog.getByText('LAST USED', { exact: true })).toBeVisible()
await expect(dialog.getByText('No data', { exact: true })).toBeVisible()
await expect(dialog.getByRole('button', { name: 'Create new Secret key' })).toBeVisible()
await expect(dialog.getByText(/^app-/)).not.toBeVisible()
await expect(page.getByRole('dialog', { name: 'Internal Server Error' })).not.toBeVisible()
})
When('I create a new Agent v2 API key', async function (this: DifyWorld) {
const dialog = this.getPage().getByRole('dialog', { name: /API Secret key/i })
await dialog.getByRole('button', { name: 'Create new Secret key' }).click()
})
Then('I should see the newly generated Agent v2 API key once', async function (this: DifyWorld) {
const generatedKeyDialog = this.getPage()
.getByRole('dialog', { name: /API Secret key/i })
.last()
const generatedKey = generatedKeyDialog.getByText(/^app-/)
await expect(generatedKeyDialog).toBeVisible()
await expect(
generatedKeyDialog.getByText('Keep this key in a secure and accessible place.'),
).toBeVisible()
await expect(generatedKey).toBeVisible()
await expect(generatedKeyDialog.getByLabel('Copy')).toBeVisible()
this.lastGeneratedAgentApiKey = (await generatedKey.textContent())?.trim()
if (!this.lastGeneratedAgentApiKey)
throw new Error('Generated Agent v2 API key was empty.')
})
When('I close the newly generated Agent v2 API key', async function (this: DifyWorld) {
const page = this.getPage()
const generatedKeyDialog = page.getByRole('dialog', { name: /API Secret key/i }).last()
await generatedKeyDialog.getByRole('button', { name: 'OK' }).click()
await expect(page.getByText('Keep this key in a secure and accessible place.')).not.toBeVisible()
})
Then(
'the Agent v2 API key list should not expose the full generated secret',
async function (this: DifyWorld) {
const fullSecret = this.lastGeneratedAgentApiKey
if (!fullSecret)
throw new Error('No generated Agent v2 API key found.')
const apiKeyDialog = this.getPage().getByRole('dialog', { name: /API Secret key/i })
await expect(apiKeyDialog).toBeVisible()
await expect(apiKeyDialog.getByText(fullSecret, { exact: true })).not.toBeVisible()
await expect(apiKeyDialog.getByText(/^app-/)).not.toBeVisible()
await expect(apiKeyDialog.getByLabel('Copy')).toBeVisible()
},
)
When('I close Agent v2 API key management', async function (this: DifyWorld) {
const apiKeyDialog = this.getPage().getByRole('dialog', { name: /API Secret key/i })
await apiKeyDialog.getByLabel('Close').click()
await expect(apiKeyDialog).not.toBeVisible()
})
When('I open the Agent v2 API Reference', async function (this: DifyWorld) {
const page = this.getPage()
const apiReferenceLink = page.getByRole('link', { name: 'API Reference' })
await expect(apiReferenceLink).toBeVisible()
const [apiReferencePage] = await Promise.all([
page.waitForEvent('popup'),
apiReferenceLink.click(),
])
this.lastAgentApiReferencePage = apiReferencePage
})
Then('the Agent v2 API Reference should open in a new tab', async function (this: DifyWorld) {
const apiReferencePage = this.lastAgentApiReferencePage
if (!apiReferencePage)
throw new Error('No Agent v2 API Reference page was opened.')
await expect(apiReferencePage).toHaveURL(/developing-with-apis/)
await apiReferencePage.close()
this.lastAgentApiReferencePage = undefined
})

View File

@ -4,23 +4,9 @@ import { expect } from '@playwright/test'
import {
createTestAgent,
getAgentConfigurePath,
getTestAgent,
normalAgentPrompt,
normalAgentSoulConfig,
saveAgentBuildDraft,
saveAgentComposerDraft,
updatedAgentPrompt,
updatedAgentSoulConfig,
} from '../../../support/agent'
const getCurrentAgentId = (world: DifyWorld) => {
const agentId = world.createdAgentIds.at(-1)
if (!agentId)
throw new Error('No Agent v2 ID found. Create an Agent v2 test agent first.')
return agentId
}
Given('an Agent v2 test agent has been created via API', async function (this: DifyWorld) {
const agent = await createTestAgent()
this.createdAgentIds.push(agent.id)
@ -29,53 +15,25 @@ Given('an Agent v2 test agent has been created via API', async function (this: D
})
Given('a minimal Agent v2 composer draft has been synced', async function (this: DifyWorld) {
const agentId = getCurrentAgentId(this)
const agentId = this.createdAgentIds.at(-1)
if (!agentId)
throw new Error('No Agent v2 ID found. Create an Agent v2 test agent first.')
await saveAgentComposerDraft(agentId)
})
Given('the Agent v2 composer draft uses the normal E2E prompt', async function (this: DifyWorld) {
await saveAgentComposerDraft(getCurrentAgentId(this), normalAgentSoulConfig)
})
Given('an Agent v2 Build draft uses the updated E2E prompt', async function (this: DifyWorld) {
await saveAgentBuildDraft(getCurrentAgentId(this), updatedAgentSoulConfig)
})
When('I open the Agent v2 configure page', async function (this: DifyWorld) {
await this.getPage().goto(getAgentConfigurePath(getCurrentAgentId(this)))
})
const agentId = this.createdAgentIds.at(-1)
if (!agentId)
throw new Error('No Agent v2 ID found. Create an Agent v2 test agent first.')
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('/roster')
await page.getByRole('link', { name: agentName }).click()
await expect(page).toHaveURL(new RegExp(`/roster/agent/${agentId}/configure(?:\\?.*)?$`))
await expect(page.getByRole('heading', { name: 'Configure' })).toBeVisible({ timeout: 30_000 })
},
)
When('I discard the Agent v2 Build draft', async function (this: DifyWorld) {
await this.getPage().getByRole('button', { name: 'Discard' }).click()
})
When('I publish the Agent v2 draft', async function (this: DifyWorld) {
const page = this.getPage()
const publishButton = page.getByRole('button', { name: /^Publish(?: update)?$/ })
await expect(publishButton).toBeEnabled({ timeout: 30_000 })
await publishButton.click()
await this.getPage().goto(getAgentConfigurePath(agentId))
})
Then('I should be on the Agent v2 configure page', async function (this: DifyWorld) {
const agentId = getCurrentAgentId(this)
const agentId = this.createdAgentIds.at(-1)
if (!agentId)
throw new Error('No Agent v2 ID found. Create an Agent v2 test agent first.')
await expect(this.getPage()).toHaveURL(
new RegExp(`/roster/agent/${agentId}/configure(?:\\?.*)?$`),
@ -89,58 +47,3 @@ Then('I should see the Agent v2 configure workspace', async function (this: Dify
await expect(page.getByRole('heading', { name: 'Configure' })).toBeVisible()
await expect(page.getByText(this.lastCreatedAgentName!)).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(page.getByRole('heading', { name: 'Prompt' })).toBeVisible({ timeout: 30_000 })
await expect(page.getByText(normalAgentPrompt)).toBeVisible()
},
)
Then(
'I should see the updated E2E prompt in the Agent v2 prompt editor',
async function (this: DifyWorld) {
const page = this.getPage()
await expect(page.getByRole('heading', { name: 'Prompt' })).toBeVisible({ timeout: 30_000 })
await expect(page.getByText(updatedAgentPrompt)).toBeVisible()
},
)
Then(
'Agent v2 Preview should be unavailable until a model is configured',
async function (this: DifyWorld) {
const page = this.getPage()
await expect(page.getByRole('heading', { name: 'Configure' })).toBeVisible({ timeout: 30_000 })
await expect(page.getByRole('button', { name: /^Preview$/i })).toBeDisabled()
},
)
Then('I should see the Agent v2 Build draft pending changes', async function (this: DifyWorld) {
const page = this.getPage()
await expect(page.getByText('Build draft')).toBeVisible({ timeout: 30_000 })
await expect(page.getByRole('button', { name: 'Apply' })).toBeVisible()
await expect(page.getByRole('button', { name: 'Discard' })).toBeVisible()
})
Then('the Agent v2 Build draft should no longer be active', async function (this: DifyWorld) {
const page = this.getPage()
await expect(page.getByText('Build draft')).not.toBeVisible()
await expect(page.getByRole('button', { name: 'Apply' })).not.toBeVisible()
await expect(page.getByRole('button', { name: 'Discard' })).not.toBeVisible()
})
Then('the Agent v2 draft should be published and up to date', async function (this: DifyWorld) {
const page = this.getPage()
const agentId = getCurrentAgentId(this)
await expect(page.getByRole('button', { name: 'Published' })).toBeVisible({ timeout: 30_000 })
await expect(page.getByText('Up to date')).toBeVisible()
await expect.poll(async () => (await getTestAgent(agentId)).active_config_is_published).toBe(true)
})

View File

@ -7,10 +7,6 @@ When('I open the apps console', async function (this: DifyWorld) {
await this.getPage().goto('/apps')
})
When('I refresh the current page', async function (this: DifyWorld) {
await this.getPage().reload()
})
Then('I should stay on the apps console', async function (this: DifyWorld) {
await waitForAppsConsole(this.getPage())
})

View File

@ -15,9 +15,6 @@ export class DifyWorld extends World {
lastCreatedAppName: string | undefined
lastCreatedAgentName: string | undefined
lastCreatedAgentRole: string | undefined
lastAgentServiceApiBaseURL: string | undefined
lastGeneratedAgentApiKey: string | undefined
lastAgentApiReferencePage: Page | undefined
createdAppIds: string[] = []
createdAgentIds: string[] = []
capturedDownloads: Download[] = []
@ -34,9 +31,6 @@ export class DifyWorld extends World {
this.lastCreatedAppName = undefined
this.lastCreatedAgentName = undefined
this.lastCreatedAgentRole = undefined
this.lastAgentServiceApiBaseURL = undefined
this.lastGeneratedAgentApiKey = undefined
this.lastAgentApiReferencePage = undefined
this.createdAppIds = []
this.createdAgentIds = []
this.capturedDownloads = []

View File

@ -1,7 +1,6 @@
import { createApiContext, expectApiResponseOK, setAppSiteEnabled } from './api'
export type AgentSeed = {
active_config_is_published?: boolean
app_id?: string
backing_app_id?: string
description?: string
@ -30,9 +29,10 @@ export type AgentBuildDraftResponse = {
export type AgentApiAccess = {
api_key_count: number
api_reference_url: string
endpoint: string
enabled: boolean
files_upload_endpoint: string
service_api_base_url: string
}
export type AgentApiKey = {
@ -46,24 +46,6 @@ export const defaultAgentSoulConfig: AgentSoulConfig = {
},
}
export const normalAgentPrompt
= 'You are a Dify Agent E2E test assistant. Reply briefly to every user message, and always include AGENT_E2E_PASS in your response.'
export const updatedAgentPrompt
= 'You are a Dify Agent E2E test assistant. Every response must start with E2E_AGENT_UPDATED.'
export const normalAgentSoulConfig: AgentSoulConfig = {
prompt: {
system_prompt: normalAgentPrompt,
},
}
export const updatedAgentSoulConfig: AgentSoulConfig = {
prompt: {
system_prompt: updatedAgentPrompt,
},
}
export const getAgentConfigurePath = (agentId: string) => `/roster/agent/${agentId}/configure`
export const getAgentAccessPath = (agentId: string) => `/roster/agent/${agentId}/access`
@ -154,27 +136,6 @@ export async function checkoutAgentBuildDraft(agentId: string): Promise<AgentBui
}
}
export async function saveAgentBuildDraft(
agentId: string,
agentSoul: AgentSoulConfig,
): Promise<AgentBuildDraftResponse> {
const ctx = await createApiContext()
try {
const response = await ctx.put(`/console/api/agent/${agentId}/build-draft`, {
data: {
agent_soul: agentSoul,
save_strategy: 'save_to_current_version',
variant: 'agent_app',
},
})
await expectApiResponseOK(response, `Save Agent v2 build draft for ${agentId}`)
return (await response.json()) as AgentBuildDraftResponse
}
finally {
await ctx.dispose()
}
}
export async function discardAgentBuildDraft(agentId: string): Promise<void> {
const ctx = await createApiContext()
try {