mirror of
https://github.com/langgenius/dify.git
synced 2026-05-21 01:07:03 +08:00
fix(workflow): address migration CI failures
Expose workflow lookup for Service API compatibility, tighten workflow template typing, and add focused command coverage for the migration PR.
This commit is contained in:
@ -23,7 +23,7 @@ def normalize_legacy_system_file_args_for_service_api(
|
||||
if not _has_legacy_file_arg(args_with_hidden_system):
|
||||
return args, None
|
||||
|
||||
workflow = AppGenerateService._get_workflow(app_model, InvokeFrom.SERVICE_API, workflow_id)
|
||||
workflow = AppGenerateService.get_workflow(app_model, InvokeFrom.SERVICE_API, workflow_id)
|
||||
return normalize_legacy_sys_files_args(graph=workflow.graph_dict, args=args_with_hidden_system)
|
||||
|
||||
|
||||
|
||||
@ -399,7 +399,7 @@ class AppGenerateService:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_workflow(cls, app_model: App, invoke_from: InvokeFrom, workflow_id: str | None = None) -> Workflow:
|
||||
def get_workflow(cls, app_model: App, invoke_from: InvokeFrom, workflow_id: str | None = None) -> Workflow:
|
||||
"""
|
||||
Get workflow
|
||||
:param app_model: app model
|
||||
@ -435,6 +435,10 @@ class AppGenerateService:
|
||||
|
||||
return workflow
|
||||
|
||||
@classmethod
|
||||
def _get_workflow(cls, app_model: App, invoke_from: InvokeFrom, workflow_id: str | None = None) -> Workflow:
|
||||
return cls.get_workflow(app_model, invoke_from, workflow_id)
|
||||
|
||||
@classmethod
|
||||
def get_response_generator(
|
||||
cls,
|
||||
|
||||
@ -55,6 +55,18 @@ def test_migrate_legacy_sys_files_workflows_rejects_non_positive_batch_size():
|
||||
)
|
||||
|
||||
|
||||
def test_migrate_legacy_sys_files_workflows_rejects_non_positive_limit():
|
||||
with pytest.raises(click.UsageError, match="limit"):
|
||||
migrate_legacy_sys_files_workflows.callback(
|
||||
batch_size=100,
|
||||
limit=0,
|
||||
start_after_id=None,
|
||||
tenant_id=None,
|
||||
app_id=None,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_build_legacy_sys_files_workflow_query_uses_keyset_pagination():
|
||||
stmt = workflow_migration_commands._build_legacy_sys_files_workflow_query(
|
||||
start_after_id="workflow-1",
|
||||
@ -96,3 +108,31 @@ def test_migrate_legacy_sys_files_workflow_batch_dry_run_rolls_back():
|
||||
assert stats.last_id == "workflow-2"
|
||||
session.rollback.assert_called_once()
|
||||
session.commit.assert_not_called()
|
||||
|
||||
|
||||
def test_migrate_legacy_sys_files_workflow_batch_commits_and_counts_failures(caplog):
|
||||
migrated_workflow = MagicMock()
|
||||
migrated_workflow.id = "workflow-1"
|
||||
migrated_workflow.migrate_legacy_sys_files_graph_in_place.return_value = True
|
||||
failing_workflow = MagicMock()
|
||||
failing_workflow.id = "workflow-2"
|
||||
failing_workflow.migrate_legacy_sys_files_graph_in_place.side_effect = RuntimeError("boom")
|
||||
session = MagicMock()
|
||||
session.scalars.return_value.all.return_value = [migrated_workflow, failing_workflow]
|
||||
|
||||
stats = workflow_migration_commands._migrate_legacy_sys_files_workflow_batch(
|
||||
session=session,
|
||||
start_after_id=None,
|
||||
batch_size=200,
|
||||
tenant_id=None,
|
||||
app_id=None,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
assert stats.scanned == 2
|
||||
assert stats.migrated == 1
|
||||
assert stats.failed == 1
|
||||
assert stats.last_id == "workflow-2"
|
||||
assert "Failed to migrate legacy" in caplog.text
|
||||
session.commit.assert_called_once()
|
||||
session.rollback.assert_not_called()
|
||||
|
||||
33
api/tests/unit_tests/commands/test_workspace.py
Normal file
33
api/tests/unit_tests/commands/test_workspace.py
Normal file
@ -0,0 +1,33 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from commands import reset_encrypt_key_pair
|
||||
from commands import workspace as workspace_commands
|
||||
|
||||
|
||||
def test_reset_encrypt_key_pair_skips_non_self_hosted(monkeypatch, capsys):
|
||||
monkeypatch.setattr(workspace_commands.dify_config, "EDITION", "CLOUD")
|
||||
|
||||
reset_encrypt_key_pair.callback()
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "only for SELF_HOSTED" in captured.out
|
||||
|
||||
|
||||
def test_reset_encrypt_key_pair_rotates_keys_and_removes_custom_provider_data(monkeypatch, capsys):
|
||||
monkeypatch.setattr(workspace_commands.dify_config, "EDITION", "SELF_HOSTED")
|
||||
monkeypatch.setattr(workspace_commands, "generate_key_pair", lambda tenant_id: f"public-key-{tenant_id}")
|
||||
tenant = MagicMock()
|
||||
tenant.id = "tenant-1"
|
||||
session = MagicMock()
|
||||
session.scalars.return_value.all.return_value = [tenant]
|
||||
session_manager = MagicMock()
|
||||
session_manager.begin.return_value.__enter__.return_value = session
|
||||
monkeypatch.setattr(workspace_commands, "sessionmaker", lambda *args, **kwargs: session_manager)
|
||||
monkeypatch.setattr(workspace_commands, "db", MagicMock(engine=object()))
|
||||
|
||||
reset_encrypt_key_pair.callback()
|
||||
|
||||
assert tenant.encrypt_public_key == "public-key-tenant-1"
|
||||
assert session.execute.call_count == 2
|
||||
captured = capsys.readouterr()
|
||||
assert "tenant-1 has been reset" in captured.out
|
||||
@ -22,7 +22,7 @@ def _legacy_file_graph() -> dict:
|
||||
def test_hidden_service_api_file_payload_maps_to_generated_start_input(mocker):
|
||||
workflow = MagicMock()
|
||||
workflow.graph_dict = _legacy_file_graph()
|
||||
get_workflow = mocker.patch.object(AppGenerateService, "_get_workflow", return_value=workflow)
|
||||
get_workflow = mocker.patch.object(AppGenerateService, "get_workflow", return_value=workflow)
|
||||
app_model = MagicMock()
|
||||
files = [{"transfer_method": "remote_url", "url": "https://example.com/a.png"}]
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import type { AnswerNodeType } from '@/app/components/workflow/nodes/answer/types'
|
||||
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
|
||||
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
@ -24,37 +26,41 @@ export const useWorkflowTemplate = () => {
|
||||
})
|
||||
|
||||
if (isChatMode) {
|
||||
const llmData: LLMNodeType = {
|
||||
...(llmDefault.defaultValue as LLMNodeType),
|
||||
desc: '',
|
||||
memory: {
|
||||
window: { enabled: false, size: 10 },
|
||||
query_prompt_template: '{{#sys.query#}}',
|
||||
},
|
||||
selected: true,
|
||||
type: llmDefault.metaData.type,
|
||||
title: t(`blocks.${llmDefault.metaData.type}`, { ns: 'workflow' }),
|
||||
}
|
||||
const { newNode: llmNode } = generateNewNode({
|
||||
id: 'llm',
|
||||
data: {
|
||||
...llmDefault.defaultValue,
|
||||
memory: {
|
||||
window: { enabled: false, size: 10 },
|
||||
query_prompt_template: '{{#sys.query#}}',
|
||||
},
|
||||
selected: true,
|
||||
type: llmDefault.metaData.type,
|
||||
title: t(`blocks.${llmDefault.metaData.type}`, { ns: 'workflow' }),
|
||||
},
|
||||
data: llmData,
|
||||
position: {
|
||||
x: START_INITIAL_POSITION.x + NODE_WIDTH_X_OFFSET,
|
||||
y: START_INITIAL_POSITION.y,
|
||||
},
|
||||
} as Parameters<typeof generateNewNode>[0])
|
||||
})
|
||||
|
||||
const answerData: AnswerNodeType = {
|
||||
...(answerDefault.defaultValue as AnswerNodeType),
|
||||
answer: `{{#${llmNode.id}.text#}}`,
|
||||
desc: '',
|
||||
type: answerDefault.metaData.type,
|
||||
title: t(`blocks.${answerDefault.metaData.type}`, { ns: 'workflow' }),
|
||||
}
|
||||
const { newNode: answerNode } = generateNewNode({
|
||||
id: 'answer',
|
||||
data: {
|
||||
...answerDefault.defaultValue,
|
||||
answer: `{{#${llmNode.id}.text#}}`,
|
||||
type: answerDefault.metaData.type,
|
||||
title: t(`blocks.${answerDefault.metaData.type}`, { ns: 'workflow' }),
|
||||
},
|
||||
data: answerData,
|
||||
position: {
|
||||
x: START_INITIAL_POSITION.x + NODE_WIDTH_X_OFFSET * 2,
|
||||
y: START_INITIAL_POSITION.y,
|
||||
},
|
||||
} as Parameters<typeof generateNewNode>[0])
|
||||
})
|
||||
|
||||
const startToLlmEdge = {
|
||||
id: `${startNode.id}-${llmNode.id}`,
|
||||
|
||||
Reference in New Issue
Block a user