Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions scripts/build_component_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,16 @@
raise ImportError(msg) from e


def _get_langflow_version():
"""Get the installed langflow version."""
def _get_lfx_version():
"""Get the installed lfx version.

Components are located in LFX, so use LFX.
"""
from importlib.metadata import version

return version("langflow")
version = version("lfx")
print(f"Retrieved LFX version: {version}")
return version


def _normalize_for_determinism(obj):
Expand Down Expand Up @@ -120,7 +125,7 @@ def _find_component_in_index(index: dict, category: str, component_name: str) ->

for item in index.get("entries", []):
# Validate entry structure: must be list/tuple with exactly 2 elements
if not (isinstance(item, (list, tuple)) and len(item) == entry_tuple_size):
if not (isinstance(item, list | tuple) and len(item) == entry_tuple_size):
msg = f"Invalid index entry format: {item}. Expected {entry_tuple_size}-element list/tuple."
raise ValueError(msg)

Expand All @@ -141,8 +146,8 @@ def _find_component_in_index(index: dict, category: str, component_name: str) ->
METADATA_KEY = "metadata"
CODE_HASH_KEY = "code_hash"
HASH_KEY = "hash" # Key used in hash_history entries
VERSION_FIRST_KEY = "version_first"
VERSION_LAST_KEY = "version_last"
VERSION_FIRST_KEY = "v_from" # Hash valid from Version (inclusive)
VERSION_LAST_KEY = "v_to" # Hash valid to version (inclusive)


def _get_component_hash(component: dict | None) -> str | None:
Expand Down Expand Up @@ -252,7 +257,7 @@ def _merge_hash_history(current_component: dict, existing_component: dict | None
Args:
current_component: Current component data with code_hash
existing_component: Existing component from disk
current_version: Current Langflow version
current_version: Current lfx version

Returns:
List of hash history entries with version ranges
Expand Down Expand Up @@ -329,7 +334,7 @@ def _load_existing_index() -> dict:


def _import_components() -> tuple[dict, int]:
"""Import all Langflow components using the async import function.
"""Import all lfx components using the async import function.

Returns:
Tuple of (modules_dict, components_count)
Expand Down Expand Up @@ -370,7 +375,7 @@ def build_component_index() -> dict:

existing_index = _load_existing_index()
modules_dict, components_count = _import_components()
current_version = _get_langflow_version()
current_version = _get_lfx_version()

# Convert modules_dict to entries format and sort for determinism
# Sort by category name (top_level) to ensure consistent ordering
Expand Down
38 changes: 19 additions & 19 deletions src/backend/tests/unit/test_component_index_hash_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ def test_merge_hash_history_new_component():

assert len(history) == 1
assert history[0]["hash"] == "abc123def456"
assert history[0]["version_first"] == "1.7.1"
assert history[0]["version_last"] == "1.7.1"
assert history[0]["v_from"] == "1.7.1"
assert history[0]["v_to"] == "1.7.1"


def test_merge_hash_history_no_previous_history():
Expand All @@ -49,8 +49,8 @@ def test_merge_hash_history_no_previous_history():

assert len(history) == 1
assert history[0]["hash"] == "abc123def456"
assert history[0]["version_first"] == "1.7.1"
assert history[0]["version_last"] == "1.7.1"
assert history[0]["v_from"] == "1.7.1"
assert history[0]["v_to"] == "1.7.1"


def test_merge_hash_history_extends_version_range():
Expand All @@ -59,7 +59,7 @@ def test_merge_hash_history_extends_version_range():
previous_component = {
"metadata": {
"code_hash": "abc123def456",
"hash_history": [{"hash": "abc123def456", "version_first": "1.7.0", "version_last": "1.7.0"}],
"hash_history": [{"hash": "abc123def456", "v_from": "1.7.0", "v_to": "1.7.0"}],
}
}
current_version = "1.7.1"
Expand All @@ -68,8 +68,8 @@ def test_merge_hash_history_extends_version_range():

assert len(history) == 1
assert history[0]["hash"] == "abc123def456"
assert history[0]["version_first"] == "1.7.0" # Preserved from previous
assert history[0]["version_last"] == "1.7.1" # Extended to current
assert history[0]["v_from"] == "1.7.0" # Preserved from previous
assert history[0]["v_to"] == "1.7.1" # Extended to current


def test_merge_hash_history_appends_on_change():
Expand All @@ -78,7 +78,7 @@ def test_merge_hash_history_appends_on_change():
previous_component = {
"metadata": {
"code_hash": "old_hash_abc",
"hash_history": [{"hash": "old_hash_abc", "version_first": "1.7.0", "version_last": "1.7.0"}],
"hash_history": [{"hash": "old_hash_abc", "v_from": "1.7.0", "v_to": "1.7.0"}],
}
}
current_version = "1.7.1"
Expand All @@ -88,12 +88,12 @@ def test_merge_hash_history_appends_on_change():
assert len(history) == 2
# Old entry preserved
assert history[0]["hash"] == "old_hash_abc"
assert history[0]["version_first"] == "1.7.0"
assert history[0]["version_last"] == "1.7.0"
assert history[0]["v_from"] == "1.7.0"
assert history[0]["v_to"] == "1.7.0"
# New entry appended
assert history[1]["hash"] == "new_hash_xyz"
assert history[1]["version_first"] == "1.7.1"
assert history[1]["version_last"] == "1.7.1"
assert history[1]["v_from"] == "1.7.1"
assert history[1]["v_to"] == "1.7.1"


def test_merge_hash_history_preserves_multiple_entries():
Expand All @@ -103,8 +103,8 @@ def test_merge_hash_history_preserves_multiple_entries():
"metadata": {
"code_hash": "hash_b",
"hash_history": [
{"hash": "hash_a", "version_first": "1.5.0", "version_last": "1.6.0"},
{"hash": "hash_b", "version_first": "1.6.1", "version_last": "1.7.0"},
{"hash": "hash_a", "v_from": "1.5.0", "v_to": "1.6.0"},
{"hash": "hash_b", "v_from": "1.6.1", "v_to": "1.7.0"},
],
}
}
Expand All @@ -116,7 +116,7 @@ def test_merge_hash_history_preserves_multiple_entries():
assert history[0]["hash"] == "hash_a"
assert history[1]["hash"] == "hash_b"
assert history[2]["hash"] == "hash_c"
assert history[2]["version_first"] == "1.7.1"
assert history[2]["v_from"] == "1.7.1"


def test_merge_hash_history_no_previous_hash():
Expand All @@ -131,8 +131,8 @@ def test_merge_hash_history_no_previous_hash():

assert len(history) == 1
assert history[0]["hash"] == "abc123"
assert history[0]["version_first"] == "1.7.1"
assert history[0]["version_last"] == "1.7.1"
assert history[0]["v_from"] == "1.7.1"
assert history[0]["v_to"] == "1.7.1"


def test_merge_hash_history_empty_hash():
Expand Down Expand Up @@ -236,8 +236,8 @@ def test_create_history_entry():
entry = _create_history_entry("abc123", "1.7.1")

assert entry["hash"] == "abc123"
assert entry["version_first"] == "1.7.1"
assert entry["version_last"] == "1.7.1"
assert entry["v_from"] == "1.7.1"
assert entry["v_to"] == "1.7.1"


# Tests for version comparison
Expand Down
4 changes: 2 additions & 2 deletions src/backend/tests/unit/test_initial_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ def test_update_projects_strips_hash_history_from_components():
"metadata": {
"code_hash": "abc123",
"hash_history": [ # This should be stripped
{"hash": "abc123", "version_first": "1.0.0", "version_last": "1.0.1"}
{"hash": "abc123", "v_from": "1.0.0", "version_last": "1.0.1"}
],
},
}
Expand Down Expand Up @@ -563,7 +563,7 @@ def test_update_projects_preserves_other_metadata():
"metadata": {
"code_hash": "abc123",
"module": "test.module",
"hash_history": [{"hash": "abc123", "version_first": "1.0.0", "version_last": "1.0.1"}],
"hash_history": [{"hash": "abc123", "v_from": "1.0.0", "v_to": "1.0.1"}],
},
}
}
Expand Down
87 changes: 87 additions & 0 deletions src/backend/tests/unit/test_starter_projects_no_hash_history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Test that starter projects do not contain hash_history in their JSON files.

This test ensures that internal component metadata (hash_history) used for tracking
component evolution in the component index does not leak into saved flow templates.
"""

import json
from pathlib import Path

import pytest


def find_hash_history_in_dict(data, path=""):
"""Recursively search for hash_history keys in nested dictionaries.

Args:
data: Dictionary or list to search
path: Current path in the data structure (for error reporting)

Returns:
List of paths where hash_history was found
"""
found_paths = []

if isinstance(data, dict):
for key, value in data.items():
current_path = f"{path}.{key}" if path else key

if key == "hash_history":
found_paths.append(current_path)

# Recursively search nested structures
found_paths.extend(find_hash_history_in_dict(value, current_path))

elif isinstance(data, list):
for i, item in enumerate(data):
current_path = f"{path}[{i}]"
found_paths.extend(find_hash_history_in_dict(item, current_path))

return found_paths


def get_starter_project_files():
"""Get all starter project JSON files."""
starter_projects_dir = (
Path(__file__).parent.parent.parent / "base" / "langflow" / "initial_setup" / "starter_projects"
)

if not starter_projects_dir.exists():
pytest.skip(f"Starter projects directory not found: {starter_projects_dir}")

json_files = list(starter_projects_dir.glob("*.json"))

if not json_files:
pytest.skip(f"No JSON files found in {starter_projects_dir}")

return json_files


@pytest.mark.parametrize("project_file", get_starter_project_files())
def test_starter_project_has_no_hash_history(project_file):
"""Test that a starter project file does not contain hash_history.

Hash_history is internal metadata for tracking component code evolution
and should only exist in component_index.json, never in saved flows.
"""
with project_file.open(encoding="utf-8") as f:
project_data = json.load(f)

# Search for any hash_history keys in the entire project structure
hash_history_paths = find_hash_history_in_dict(project_data)

assert not hash_history_paths, (
f"Found hash_history in {project_file.name} at paths: {hash_history_paths}\n"
"hash_history is internal component metadata and should not be in saved flows. "
"It should only exist in component_index.json for tracking component evolution."
)


def test_all_starter_projects_loaded():
"""Sanity check that we're actually testing starter projects."""
project_files = get_starter_project_files()

# We should have multiple starter projects
assert len(project_files) > 0, "No starter project files found to test"

# Print count for visibility
Comment on lines +80 to +87
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Complete the implementation.

The comment on line 87 suggests there should be a print statement for visibility, but the implementation is incomplete.

📝 Suggested completion
 def test_all_starter_projects_loaded():
     """Sanity check that we're actually testing starter projects."""
     project_files = get_starter_project_files()
 
     # We should have multiple starter projects
     assert len(project_files) > 0, "No starter project files found to test"
 
     # Print count for visibility
+    print(f"Testing {len(project_files)} starter project files")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_all_starter_projects_loaded():
"""Sanity check that we're actually testing starter projects."""
project_files = get_starter_project_files()
# We should have multiple starter projects
assert len(project_files) > 0, "No starter project files found to test"
# Print count for visibility
def test_all_starter_projects_loaded():
"""Sanity check that we're actually testing starter projects."""
project_files = get_starter_project_files()
# We should have multiple starter projects
assert len(project_files) > 0, "No starter project files found to test"
# Print count for visibility
print(f"Testing {len(project_files)} starter project files")
🤖 Prompt for AI Agents
In @src/backend/tests/unit/test_starter_projects_no_hash_history.py around lines
80 - 87, The test_all_starter_projects_loaded test is missing the visibility
print mentioned in the comment; update the test to print the count (and
optionally file names) of starter projects after calling
get_starter_project_files(); e.g., invoke print(f"Found {len(project_files)}
starter project files") (or print the list) before the final assertions so test
output shows how many/which starter projects were discovered while keeping the
existing assertion using len(project_files) > 0.

2 changes: 1 addition & 1 deletion src/lfx/src/lfx/_assets/component_index.json

Large diffs are not rendered by default.

Loading
Loading