mirror of
https://github.com/langgenius/dify.git
synced 2026-05-29 05:07:55 +08:00
Co-authored-by: XW <wei.xu1@wiz.ai> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
385 lines
14 KiB
Python
385 lines
14 KiB
Python
from commands.data_migration import (
|
|
CONFLICT_STRATEGY_CHOICES,
|
|
ID_STRATEGY_CHOICES,
|
|
_confirm_wizard_summary,
|
|
_print_auto_tools,
|
|
_print_final_tool_selection,
|
|
_print_wizard_step,
|
|
_prompt_additional_tools,
|
|
_prompt_output_file,
|
|
_prompt_tool_category,
|
|
_resolve_mcp_tool_names,
|
|
migration_data_wizard,
|
|
parse_index_selection,
|
|
)
|
|
|
|
|
|
def test_parse_index_selection_supports_all():
|
|
assert parse_index_selection("all", ["a", "b", "c"]) == ["a", "b", "c"]
|
|
|
|
|
|
def test_wizard_command_uses_app_migration_name():
|
|
assert migration_data_wizard.name == "app-migration-wizard"
|
|
|
|
|
|
def test_parse_index_selection_supports_comma_indexes():
|
|
assert parse_index_selection("1, 3", ["a", "b", "c"]) == ["a", "c"]
|
|
|
|
|
|
def test_print_wizard_step_adds_separator(monkeypatch):
|
|
output_lines = []
|
|
|
|
monkeypatch.setattr("commands.data_migration.click.echo", output_lines.append)
|
|
|
|
_print_wizard_step("App Selection")
|
|
|
|
assert output_lines == ["", "==== App Selection ===="]
|
|
|
|
|
|
def test_conflict_strategy_choices_exclude_replace():
|
|
assert CONFLICT_STRATEGY_CHOICES == ["fail", "skip", "update"]
|
|
|
|
|
|
def test_prompt_app_ids_explains_comma_selection_and_default(monkeypatch):
|
|
from commands.data_migration import _prompt_app_ids
|
|
|
|
prompts = []
|
|
output_lines = []
|
|
apps = [
|
|
type("App", (), {"id": "app-1", "name": "embedded", "mode": "workflow"})(),
|
|
type("App", (), {"id": "app-2", "name": "main", "mode": "advanced-chat"})(),
|
|
]
|
|
|
|
def capture_prompt(text, **kwargs):
|
|
prompts.append((text, kwargs))
|
|
return "1,2"
|
|
|
|
monkeypatch.setattr("commands.data_migration.click.echo", output_lines.append)
|
|
monkeypatch.setattr("commands.data_migration.click.prompt", capture_prompt)
|
|
|
|
assert _prompt_app_ids(apps) == ["app-1", "app-2"]
|
|
assert prompts == [("Select apps by number, comma-separated numbers, or all", {"default": "all"})]
|
|
assert "Currently supported app types: workflow and chatflow." in output_lines
|
|
|
|
|
|
def test_prompt_tool_category_marks_auto_discovered_tools(monkeypatch):
|
|
output_lines = []
|
|
|
|
monkeypatch.setattr("commands.data_migration.click.echo", output_lines.append)
|
|
monkeypatch.setattr("commands.data_migration.click.prompt", lambda *args, **kwargs: "")
|
|
|
|
selected = _prompt_tool_category(
|
|
"Custom API tools",
|
|
[("weather", "weather", "tool-id"), ("calendar", "calendar", "calendar-id")],
|
|
auto_tools={"weather": "tool-id"},
|
|
)
|
|
|
|
assert selected == []
|
|
assert "1. [auto] weather (tool-id)" in output_lines
|
|
assert "2. [ ] calendar (calendar-id)" in output_lines
|
|
assert output_lines[:2] == ["", "==== Custom API tools ===="]
|
|
|
|
|
|
def test_prompt_tool_category_explains_comma_selection_and_default(monkeypatch):
|
|
prompts = []
|
|
|
|
def capture_prompt(text, **kwargs):
|
|
prompts.append((text, kwargs))
|
|
return ""
|
|
|
|
monkeypatch.setattr("commands.data_migration.click.echo", lambda *_args, **_kwargs: None)
|
|
monkeypatch.setattr("commands.data_migration.click.prompt", capture_prompt)
|
|
|
|
selected = _prompt_tool_category(
|
|
"Custom API tools",
|
|
[("weather", "weather", "tool-id")],
|
|
auto_tools={},
|
|
)
|
|
|
|
assert selected == []
|
|
assert prompts == [
|
|
(
|
|
"Select custom api tools by number, comma-separated numbers, all, or empty",
|
|
{"default": "", "show_default": "empty"},
|
|
)
|
|
]
|
|
|
|
|
|
def test_prompt_output_file_shows_default(monkeypatch):
|
|
prompts = []
|
|
|
|
def capture_prompt(text, **kwargs):
|
|
prompts.append((text, kwargs))
|
|
return "migration-data.json"
|
|
|
|
monkeypatch.setattr("commands.data_migration.click.prompt", capture_prompt)
|
|
|
|
assert _prompt_output_file() == ("migration-data.json", False)
|
|
assert prompts[0][0] == "Output path"
|
|
assert prompts[0][1]["show_default"] is True
|
|
|
|
|
|
def test_prompt_tool_category_marks_auto_by_detail_and_supports_multi_select(monkeypatch):
|
|
monkeypatch.setattr("commands.data_migration.click.echo", lambda *_args, **_kwargs: None)
|
|
monkeypatch.setattr("commands.data_migration.click.prompt", lambda *args, **kwargs: "1,2")
|
|
|
|
selected = _prompt_tool_category(
|
|
"Workflow tools",
|
|
[("tool-1", "embedded", "app-1"), ("tool-2", "other", "app-2")],
|
|
auto_tools={"embedded": "app-1"},
|
|
)
|
|
|
|
assert selected == ["tool-1", "tool-2"]
|
|
|
|
|
|
def test_prompt_tool_category_marks_auto_by_value():
|
|
output_lines = []
|
|
|
|
from commands import data_migration
|
|
|
|
original_echo = data_migration.click.echo
|
|
original_prompt = data_migration.click.prompt
|
|
data_migration.click.echo = output_lines.append
|
|
data_migration.click.prompt = lambda *args, **kwargs: ""
|
|
try:
|
|
_prompt_tool_category(
|
|
"Workflow tools",
|
|
[("tool-1", "embedded_workflow_as_tool", "tool-1")],
|
|
auto_tools={"embedded_workflow_as_tool": "tool-1"},
|
|
)
|
|
finally:
|
|
data_migration.click.echo = original_echo
|
|
data_migration.click.prompt = original_prompt
|
|
|
|
assert "1. [auto] embedded_workflow_as_tool (tool-1)" in output_lines
|
|
|
|
|
|
def test_print_auto_tools_lists_each_category(monkeypatch):
|
|
output_lines = []
|
|
|
|
monkeypatch.setattr("commands.data_migration.click.echo", output_lines.append)
|
|
|
|
_print_auto_tools(
|
|
{
|
|
"api_tools": {"weather": "3bac3aa9-dd87-4351-9459-a7099137b028"},
|
|
"workflow_tools": {"embedded_workflow_as_tool": "e6024578-41b7-4fb5-a81f-9201358e5835"},
|
|
"mcp_tools": {},
|
|
}
|
|
)
|
|
|
|
assert "Automatically discovered tools:" in output_lines
|
|
assert "Custom API tools" in output_lines
|
|
assert "- weather: 3bac3aa9-dd87-4351-9459-a7099137b028" in output_lines
|
|
assert "Workflow tools" in output_lines
|
|
assert "- embedded_workflow_as_tool: e6024578-41b7-4fb5-a81f-9201358e5835" in output_lines
|
|
assert "MCP tools" in output_lines
|
|
assert "- none" in output_lines
|
|
|
|
|
|
def test_resolve_mcp_tool_names_does_not_compare_non_uuid_identifier_to_uuid_id(monkeypatch):
|
|
statements = []
|
|
|
|
def capture_scalar(statement):
|
|
statements.append(str(statement))
|
|
|
|
monkeypatch.setattr("commands.data_migration.db.session.scalar", capture_scalar)
|
|
|
|
assert _resolve_mcp_tool_names("49a99e46-bc2c-4885-91fa-47615f6192b5", {"my-test-mcp": "my-test-mcp"}) == {
|
|
"my-test-mcp": "my-test-mcp"
|
|
}
|
|
assert "tool_mcp_providers.id =" not in statements[0]
|
|
assert "tool_mcp_providers.server_identifier =" in statements[0]
|
|
|
|
|
|
def test_prompt_additional_tools_prints_final_selection_when_skipped(monkeypatch):
|
|
output_lines = []
|
|
confirm_prompts = []
|
|
|
|
def capture_confirm(prompt, **kwargs):
|
|
confirm_prompts.append((prompt, kwargs))
|
|
return False
|
|
|
|
monkeypatch.setattr("commands.data_migration.click.confirm", capture_confirm)
|
|
monkeypatch.setattr("commands.data_migration.click.echo", output_lines.append)
|
|
|
|
selected = _prompt_additional_tools(
|
|
"tenant-id",
|
|
{
|
|
"api_tools": {"weather": "3bac3aa9-dd87-4351-9459-a7099137b028"},
|
|
"workflow_tools": {},
|
|
"mcp_tools": {},
|
|
},
|
|
)
|
|
|
|
assert selected == {"api_tools": [], "workflow_tools": [], "mcp_tools": []}
|
|
assert confirm_prompts == [
|
|
("Export additional tools manually? [y/n, default: n]", {"default": False, "show_default": False})
|
|
]
|
|
assert "Final tools to export:" in output_lines
|
|
assert "- [auto] weather: 3bac3aa9-dd87-4351-9459-a7099137b028" in output_lines
|
|
|
|
|
|
def test_final_tool_selection_deduplicates_manual_tool_already_auto(monkeypatch):
|
|
output_lines = []
|
|
|
|
monkeypatch.setattr("commands.data_migration.click.echo", output_lines.append)
|
|
|
|
_print_final_tool_selection(
|
|
{
|
|
"api_tools": {},
|
|
"workflow_tools": {"embedded_workflow_as_tool": "e6024578-41b7-4fb5-a81f-9201358e5835"},
|
|
"mcp_tools": {},
|
|
},
|
|
{
|
|
"api_tools": [],
|
|
"workflow_tools": ["e6024578-41b7-4fb5-a81f-9201358e5835"],
|
|
"mcp_tools": [],
|
|
},
|
|
{"e6024578-41b7-4fb5-a81f-9201358e5835": "embedded_workflow_as_tool: e6024578"},
|
|
)
|
|
|
|
assert "- [auto] embedded_workflow_as_tool: e6024578-41b7-4fb5-a81f-9201358e5835" in output_lines
|
|
assert not any(line.startswith("- [manual]") for line in output_lines)
|
|
|
|
|
|
def test_prompt_output_file_rejects_yes_no_typo(monkeypatch):
|
|
import click
|
|
import pytest
|
|
|
|
monkeypatch.setattr("commands.data_migration.click.prompt", lambda *args, **kwargs: "y")
|
|
|
|
with pytest.raises(click.ClickException, match="Output path must be a file path"):
|
|
_prompt_output_file()
|
|
|
|
|
|
def test_confirm_wizard_summary_shows_conflict_strategy(monkeypatch):
|
|
output_lines = []
|
|
confirm_prompts = []
|
|
|
|
monkeypatch.setattr("commands.data_migration.click.echo", output_lines.append)
|
|
monkeypatch.setattr(
|
|
"commands.data_migration.click.confirm",
|
|
lambda prompt, **kwargs: confirm_prompts.append((prompt, kwargs)) or True,
|
|
)
|
|
|
|
_confirm_wizard_summary(
|
|
tenant_name="admin's Workspace",
|
|
app_names=["main_chatflow"],
|
|
auto_tools={"api_tools": {}, "workflow_tools": {}, "mcp_tools": {}},
|
|
additional_tools={"api_tools": [], "workflow_tools": [], "mcp_tools": []},
|
|
manual_labels={},
|
|
include_referenced_tools=True,
|
|
include_secrets=False,
|
|
create_tokens=True,
|
|
id_strategy="preserve-id",
|
|
conflict_strategy="fail",
|
|
output_file="migration-data.json",
|
|
)
|
|
|
|
assert "id strategy: preserve-id" in output_lines
|
|
assert "conflict strategy: fail" in output_lines
|
|
assert confirm_prompts == [("Write migration package? [y/n, default: y]", {"default": True, "show_default": False})]
|
|
|
|
|
|
def test_confirm_wizard_summary_shows_final_deduplicated_tool_selection(monkeypatch):
|
|
output_lines = []
|
|
|
|
monkeypatch.setattr("commands.data_migration.click.echo", output_lines.append)
|
|
monkeypatch.setattr("commands.data_migration.click.confirm", lambda *args, **kwargs: True)
|
|
|
|
_confirm_wizard_summary(
|
|
tenant_name="admin's Workspace",
|
|
app_names=["main_chatflow"],
|
|
auto_tools={
|
|
"api_tools": {"weather": "weather-id"},
|
|
"workflow_tools": {"embedded_workflow_as_tool": "workflow-tool-id"},
|
|
"mcp_tools": {},
|
|
},
|
|
additional_tools={
|
|
"api_tools": ["weather-id", "calendar"],
|
|
"workflow_tools": [],
|
|
"mcp_tools": ["mcp-id"],
|
|
},
|
|
manual_labels={
|
|
"calendar": "calendar: calendar-id",
|
|
"mcp-id": "my-test-mcp: mcp-id",
|
|
},
|
|
include_referenced_tools=True,
|
|
include_secrets=False,
|
|
create_tokens=False,
|
|
id_strategy="preserve-id",
|
|
conflict_strategy="update",
|
|
output_file="migration-data.json",
|
|
)
|
|
|
|
assert "Final tools to export:" in output_lines
|
|
assert "Custom API tools" in output_lines
|
|
assert "- [auto] weather: weather-id" in output_lines
|
|
assert "- [manual] calendar: calendar-id" in output_lines
|
|
assert "Workflow tools" in output_lines
|
|
assert "- [auto] embedded_workflow_as_tool: workflow-tool-id" in output_lines
|
|
assert "MCP tools" in output_lines
|
|
assert "- [manual] my-test-mcp: mcp-id" in output_lines
|
|
assert not any(line.startswith("additional api tools:") for line in output_lines)
|
|
assert not any(line.startswith("additional workflow tools:") for line in output_lines)
|
|
assert not any(line.startswith("additional mcp tools:") for line in output_lines)
|
|
assert "- [manual] weather-id" not in output_lines
|
|
|
|
|
|
def test_import_options_prompts_explain_secrets_reuse_and_conflicts(monkeypatch):
|
|
from commands.data_migration import _prompt_import_options
|
|
|
|
output_lines = []
|
|
confirm_prompts = []
|
|
prompt_calls = []
|
|
|
|
def capture_confirm(prompt, **kwargs):
|
|
confirm_prompts.append((prompt, kwargs))
|
|
return False
|
|
|
|
def capture_prompt(prompt, **kwargs):
|
|
prompt_calls.append((prompt, kwargs))
|
|
return kwargs["default"]
|
|
|
|
monkeypatch.setattr("commands.data_migration.click.echo", output_lines.append)
|
|
monkeypatch.setattr("commands.data_migration.click.confirm", capture_confirm)
|
|
monkeypatch.setattr("commands.data_migration.click.prompt", capture_prompt)
|
|
|
|
include_secrets, create_tokens, id_strategy, conflict_strategy = _prompt_import_options()
|
|
|
|
assert include_secrets is False
|
|
assert create_tokens is False
|
|
assert id_strategy == "preserve-id"
|
|
assert conflict_strategy == "update"
|
|
assert "Secrets include workflow/app DSL secret values, custom API tool credentials," in output_lines
|
|
assert "-- Secrets --" in output_lines
|
|
assert "If you choose no, credentials are omitted or masked," in output_lines
|
|
assert "-- App API Tokens --" in output_lines
|
|
assert "When enabled, import will create an app API token if the imported app has none," in output_lines
|
|
assert "or reuse an existing app API token if one already exists." in output_lines
|
|
assert "-- ID Strategy --" in output_lines
|
|
assert "ID strategy controls whether imported app and tool IDs preserve source IDs" in output_lines
|
|
assert "or use target-generated IDs." in output_lines
|
|
assert "preserve-id: keep source IDs where the target service supports it." in output_lines
|
|
assert (
|
|
"generate-new-id: let the target environment generate new IDs and rewrite references via mapping."
|
|
in output_lines
|
|
)
|
|
assert "-- Conflict Strategy --" in output_lines
|
|
assert "Conflict strategy controls what import does when a target resource already exists." in output_lines
|
|
assert "fail: stop at the first conflict; previously committed resources are not rolled back." in output_lines
|
|
assert "skip: keep the existing target resource and skip importing that resource." in output_lines
|
|
assert "update: update the existing target resource in place." in output_lines
|
|
assert confirm_prompts == [
|
|
("Include secrets in output JSON? [y/n, default: n]", {"default": False, "show_default": False}),
|
|
("Create or reuse app API tokens during import? [y/n, default: n]", {"default": False, "show_default": False}),
|
|
]
|
|
assert prompt_calls[0][0] == "Import ID strategy. Enter one of: preserve-id, generate-new-id"
|
|
assert prompt_calls[0][1]["default"] == "preserve-id"
|
|
assert prompt_calls[0][1]["show_default"] is True
|
|
assert prompt_calls[0][1]["type"].choices == ID_STRATEGY_CHOICES
|
|
assert prompt_calls[1][0] == "Import conflict strategy. Enter one of: fail, skip, update"
|
|
assert prompt_calls[1][1]["default"] == "update"
|
|
assert prompt_calls[1][1]["show_default"] is True
|
|
assert prompt_calls[1][1]["type"].choices == CONFLICT_STRATEGY_CHOICES
|