From c1d417adf2de55a0d550cc3faebf6f1d0b6ad5eb Mon Sep 17 00:00:00 2001 From: Cristhian Zanforlin Lousa Date: Tue, 22 Jul 2025 14:45:15 -0300 Subject: [PATCH] fix: Improve duplicate flow name handling and add comprehensive tests (#8962) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ (flows.py): Improve flow naming logic to handle duplicate names more effectively and accurately 📝 (test_rename_flow_to_save.py): Add unit tests to ensure correct numbering and handling of duplicate flow names * [autofix.ci] apply automated fixes * ✨ (test_rename_flow_to_save.py): refactor test functions to remove unnecessary session parameter and improve code readability * 📝 (flows.py): improve regex pattern to extract numbers only from flows following a specific naming convention to avoid extracting numbers from the original flow name if it contains parentheses * [autofix.ci] apply automated fixes * 📝 (flows.py): improve comments for better readability and understanding of regex usage in extracting numbers from flow names --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/backend/base/langflow/api/v1/flows.py | 12 +- .../unit/api/v1/test_rename_flow_to_save.py | 161 ++++++++++++++++++ 2 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 src/backend/tests/unit/api/v1/test_rename_flow_to_save.py diff --git a/src/backend/base/langflow/api/v1/flows.py b/src/backend/base/langflow/api/v1/flows.py index 9f1766a5c..a51334fbc 100644 --- a/src/backend/base/langflow/api/v1/flows.py +++ b/src/backend/base/langflow/api/v1/flows.py @@ -83,7 +83,15 @@ async def _new_flow( ) ).all() if flows: - extract_number = re.compile(r"\((\d+)\)$") + # Use regex to extract numbers only from flows that follow the copy naming pattern: + # "{original_name} ({number})" + # This avoids extracting numbers from the original flow name if it naturally contains parentheses + # + # Examples: + # - For flow "My Flow": matches "My Flow (1)", "My Flow (2)" → extracts 1, 2 + # - For flow "Analytics (Q1)": matches "Analytics (Q1) (1)" → extracts 1 + # but does NOT match "Analytics (Q1)" → avoids extracting the original "1" + extract_number = re.compile(rf"^{re.escape(flow.name)} \((\d+)\)$") numbers = [] for _flow in flows: result = extract_number.search(_flow.name) @@ -91,6 +99,8 @@ async def _new_flow( numbers.append(int(result.groups(1)[0])) if numbers: flow.name = f"{flow.name} ({max(numbers) + 1})" + else: + flow.name = f"{flow.name} (1)" else: flow.name = f"{flow.name} (1)" # Now check if the endpoint is unique diff --git a/src/backend/tests/unit/api/v1/test_rename_flow_to_save.py b/src/backend/tests/unit/api/v1/test_rename_flow_to_save.py new file mode 100644 index 000000000..35b63d933 --- /dev/null +++ b/src/backend/tests/unit/api/v1/test_rename_flow_to_save.py @@ -0,0 +1,161 @@ +import pytest +from fastapi import status +from httpx import AsyncClient + + +@pytest.mark.asyncio +async def test_duplicate_flow_name_basic(client: AsyncClient, logged_in_headers): + """Test that duplicate flow names get numbered correctly.""" + base_flow = { + "name": "Test Flow", + "description": "Test flow description", + "data": {}, + "is_component": False, + } + + # Create first flow + response1 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers) + assert response1.status_code == status.HTTP_201_CREATED + assert response1.json()["name"] == "Test Flow" + + # Create second flow with same name - should become "Test Flow (1)" + response2 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers) + assert response2.status_code == status.HTTP_201_CREATED + assert response2.json()["name"] == "Test Flow (1)" + + # Create third flow with same name - should become "Test Flow (2)" + response3 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers) + assert response3.status_code == status.HTTP_201_CREATED + assert response3.json()["name"] == "Test Flow (2)" + + +@pytest.mark.asyncio +async def test_duplicate_flow_name_with_numbers_in_original(client: AsyncClient, logged_in_headers): + """Test duplication of flows with numbers in their original name.""" + base_flow = { + "name": "Untitled document (7)", + "description": "Test flow description", + "data": {}, + "is_component": False, + } + + # Create first flow + response1 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers) + assert response1.status_code == status.HTTP_201_CREATED + assert response1.json()["name"] == "Untitled document (7)" + + # Create second flow with same name - should become "Untitled document (7) (1)" + response2 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers) + assert response2.status_code == status.HTTP_201_CREATED + assert response2.json()["name"] == "Untitled document (7) (1)" + + # Create third flow with same name - should become "Untitled document (7) (2)" + response3 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers) + assert response3.status_code == status.HTTP_201_CREATED + assert response3.json()["name"] == "Untitled document (7) (2)" + + +@pytest.mark.asyncio +async def test_duplicate_flow_name_with_non_numeric_suffixes(client: AsyncClient, logged_in_headers): + """Test that non-numeric suffixes don't interfere with numbering.""" + base_flow = { + "name": "My Flow", + "description": "Test flow description", + "data": {}, + "is_component": False, + } + + # Create first flow + response1 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers) + assert response1.status_code == status.HTTP_201_CREATED + assert response1.json()["name"] == "My Flow" + + # Create flow with non-numeric suffix + backup_flow = base_flow.copy() + backup_flow["name"] = "My Flow (Backup)" + response2 = await client.post("api/v1/flows/", json=backup_flow, headers=logged_in_headers) + assert response2.status_code == status.HTTP_201_CREATED + assert response2.json()["name"] == "My Flow (Backup)" + + # Create another flow with original name - should become "My Flow (1)" + # because "My Flow (Backup)" doesn't match the numeric pattern + response3 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers) + assert response3.status_code == status.HTTP_201_CREATED + assert response3.json()["name"] == "My Flow (1)" + + +@pytest.mark.asyncio +async def test_duplicate_flow_name_gaps_in_numbering(client: AsyncClient, logged_in_headers): + """Test that gaps in numbering are handled correctly (uses max + 1).""" + base_flow = { + "name": "Gapped Flow", + "description": "Test flow description", + "data": {}, + "is_component": False, + } + + # Create original flow + response1 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers) + assert response1.status_code == status.HTTP_201_CREATED + assert response1.json()["name"] == "Gapped Flow" + + # Create numbered flows with gaps + numbered_flows = [ + "Gapped Flow (1)", + "Gapped Flow (5)", # Gap: 2, 3, 4 missing + "Gapped Flow (7)", # Gap: 6 missing + ] + + for flow_name in numbered_flows: + numbered_flow = base_flow.copy() + numbered_flow["name"] = flow_name + response = await client.post("api/v1/flows/", json=numbered_flow, headers=logged_in_headers) + assert response.status_code == status.HTTP_201_CREATED + assert response.json()["name"] == flow_name + + # Create another duplicate - should use max(1,5,7) + 1 = 8 + response_final = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers) + assert response_final.status_code == status.HTTP_201_CREATED + assert response_final.json()["name"] == "Gapped Flow (8)" + + +@pytest.mark.asyncio +async def test_duplicate_flow_name_special_characters(client: AsyncClient, logged_in_headers): + """Test duplication with special characters in flow names.""" + base_flow = { + "name": "Flow-with_special@chars!", + "description": "Test flow description", + "data": {}, + "is_component": False, + } + + # Create first flow + response1 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers) + assert response1.status_code == status.HTTP_201_CREATED + assert response1.json()["name"] == "Flow-with_special@chars!" + + # Create duplicate - should properly escape special characters in regex + response2 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers) + assert response2.status_code == status.HTTP_201_CREATED + assert response2.json()["name"] == "Flow-with_special@chars! (1)" + + +@pytest.mark.asyncio +async def test_duplicate_flow_name_regex_patterns(client: AsyncClient, logged_in_headers): + """Test that flow names containing regex special characters work correctly.""" + base_flow = { + "name": "Flow (.*) [test]", + "description": "Test flow description", + "data": {}, + "is_component": False, + } + + # Create first flow + response1 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers) + assert response1.status_code == status.HTTP_201_CREATED + assert response1.json()["name"] == "Flow (.*) [test]" + + # Create duplicate + response2 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers) + assert response2.status_code == status.HTTP_201_CREATED + assert response2.json()["name"] == "Flow (.*) [test] (1)"