From d2648b95c78ae99e8d6ec2544c656d87d72f9dd5 Mon Sep 17 00:00:00 2001 From: Cristhian Zanforlin Lousa Date: Wed, 24 Jun 2026 13:59:57 -0300 Subject: [PATCH] test: guard temporarily-disabled bundles in component/template tests + skip Windows-flaky model spec (#13807) test: guard disabled bundles in component/template tests and skip Windows-flaky model spec --- .../components/test_all_modules_importable.py | 49 ++++++++++++++++++- .../unit/template/test_starter_projects.py | 40 +++++++++++++++ .../core/features/modelInputComponent.spec.ts | 7 +++ 3 files changed, 95 insertions(+), 1 deletion(-) diff --git a/src/backend/tests/unit/components/test_all_modules_importable.py b/src/backend/tests/unit/components/test_all_modules_importable.py index 951ac8f312..3fe10f3ff2 100644 --- a/src/backend/tests/unit/components/test_all_modules_importable.py +++ b/src/backend/tests/unit/components/test_all_modules_importable.py @@ -21,6 +21,27 @@ import pytest from langflow import components from lfx.interface.components import _warm_circular_imports +# Provider components ship in bundle distributions that can be temporarily +# unpublished (see the re-enable note in pyproject.toml). While a bundle is +# absent its category stays in +# ``components.__all__`` but cannot be imported, so these tests skip it instead +# of failing. Each guard is computed from the live import result, so it turns +# back into a no-op the moment the bundle is restored -- no revert needed. +_OPTIONAL_BUNDLE_CATEGORIES = ("openai", "datastax", "oracle") + + +def _unavailable_bundle_categories() -> set[str]: + unavailable = set() + for category_name in _OPTIONAL_BUNDLE_CATEGORIES: + try: + getattr(components, category_name) + except Exception: + unavailable.add(category_name) + return unavailable + + +UNAVAILABLE_BUNDLE_CATEGORIES = _unavailable_bundle_categories() + class TestAllModulesImportable: """Test that all component modules are importable.""" @@ -30,6 +51,8 @@ class TestAllModulesImportable: failed_imports = [] for category_name in components.__all__: + if category_name in UNAVAILABLE_BUNDLE_CATEGORIES: + continue try: category_module = getattr(components, category_name) assert category_module is not None, f"Category {category_name} is None" @@ -64,6 +87,8 @@ class TestAllModulesImportable: print(f"Testing component imports across {len(components.__all__)} categories") # noqa: T201 for category_name in components.__all__: + if category_name in UNAVAILABLE_BUNDLE_CATEGORIES: + continue try: category_module = getattr(components, category_name) @@ -110,6 +135,8 @@ class TestAllModulesImportable: failed_mappings = [] for category_name in components.__all__: + if category_name in UNAVAILABLE_BUNDLE_CATEGORIES: + continue try: category_module = getattr(components, category_name) @@ -147,6 +174,8 @@ class TestAllModulesImportable: failed_imports = [] for module_name, component_name in traditional_imports: + if module_name.rsplit(".", 1)[-1] in UNAVAILABLE_BUNDLE_CATEGORIES: + continue try: module = importlib.import_module(module_name) component = getattr(module, component_name) @@ -164,6 +193,8 @@ class TestAllModulesImportable: failed_modules = [] for category_name in components.__all__: + if category_name in UNAVAILABLE_BUNDLE_CATEGORIES: + continue try: category_module = getattr(components, category_name) @@ -201,6 +232,8 @@ class TestAllModulesImportable: for order in import_orders: try: for category_name in order: + if category_name in UNAVAILABLE_BUNDLE_CATEGORIES: + continue category_module = getattr(components, category_name) # Access a component to trigger dynamic import if hasattr(category_module, "__all__") and category_module.__all__: @@ -220,6 +253,8 @@ class TestAllModulesImportable: ] for category_name, component_name in test_cases: + if category_name in UNAVAILABLE_BUNDLE_CATEGORIES: + continue category_module = getattr(components, category_name) # First access @@ -240,6 +275,8 @@ class TestAllModulesImportable: ] for category_name, component_name in test_cases: + if category_name in UNAVAILABLE_BUNDLE_CATEGORIES: + continue category_module = getattr(components, category_name) with pytest.raises(AttributeError, match=f"has no attribute '{component_name}'"): @@ -249,12 +286,15 @@ class TestAllModulesImportable: """Test that __dir__ functionality works for all modules.""" # Test main components module main_dir = dir(components) - assert "openai" in main_dir + if "openai" not in UNAVAILABLE_BUNDLE_CATEGORIES: + assert "openai" in main_dir assert "data" in main_dir assert "models_and_agents" in main_dir # Test category modules for category_name in ["openai", "data", "helpers"]: + if category_name in UNAVAILABLE_BUNDLE_CATEGORIES: + continue category_module = getattr(components, category_name) category_dir = dir(category_module) @@ -272,6 +312,8 @@ class TestAllModulesImportable: ] for category_name, component_name in test_components: + if category_name in UNAVAILABLE_BUNDLE_CATEGORIES: + continue category_module = getattr(components, category_name) component = getattr(category_module, component_name) @@ -410,6 +452,11 @@ class TestDirectModuleImports: "altk", "langchain_ibm", "ibm_watsonx_ai", + # Bundle distributions that can be temporarily unpublished; + # the relocation shim raises with the distribution name. + "lfx-openai", + "lfx-datastax", + "lfx-oracle", ] ): return ("skipped", modname, "missing optional dependency") diff --git a/src/backend/tests/unit/template/test_starter_projects.py b/src/backend/tests/unit/template/test_starter_projects.py index 595dad4d4b..3b9c7b193c 100644 --- a/src/backend/tests/unit/template/test_starter_projects.py +++ b/src/backend/tests/unit/template/test_starter_projects.py @@ -22,6 +22,30 @@ from langflow.utils.template_validation import ( validate_template_structure, ) +# Component modules that ship in bundle distributions which can be temporarily +# unpublished (see the re-enable note in pyproject.toml). A template that wires +# one of these cannot be built while its bundle is absent, so the build/execute +# tests skip it instead of failing. The check imports the live distribution, so +# it turns back into a no-op the moment the bundle is restored -- no revert. +_OPTIONAL_BUNDLE_MODULES = { + "lfx.components.datastax": "lfx_datastax", + "lfx.components.openai": "lfx_openai", + "lfx.components.oracle": "lfx_oracle", +} + + +def _template_unavailable_bundle(template_data: dict) -> str | None: + """Return the missing distribution if the template needs an absent bundle, else None.""" + for node in template_data.get("data", {}).get("nodes", []): + module_path = node.get("data", {}).get("node", {}).get("metadata", {}).get("module", "") + for prefix, distribution in _OPTIONAL_BUNDLE_MODULES.items(): + if module_path.startswith(prefix): + try: + import_module(distribution) + except ImportError: + return distribution + return None + def get_starter_projects_path() -> Path: """Get path to starter projects directory.""" @@ -83,6 +107,10 @@ class TestStarterProjects: with template_file.open(encoding="utf-8") as f: template_data = json.load(f) + unavailable = _template_unavailable_bundle(template_data) + if unavailable: + pytest.skip(f"{template_file.name} needs the temporarily-unavailable {unavailable} bundle") + errors = validate_flow_can_build(template_data, template_file.name) if errors: error_msg = "\n".join(errors) @@ -95,6 +123,10 @@ class TestStarterProjects: with template_file.open(encoding="utf-8") as f: template_data = json.load(f) + unavailable = _template_unavailable_bundle(template_data) + if unavailable: + pytest.skip(f"{template_file.name} needs the temporarily-unavailable {unavailable} bundle") + errors = await validate_flow_execution(client, template_data, template_file.name, logged_in_headers) if errors: error_msg = "\n".join(errors) @@ -108,6 +140,10 @@ class TestStarterProjects: with template_file.open(encoding="utf-8") as f: template_data = json.load(f) + unavailable = _template_unavailable_bundle(template_data) + if unavailable: + pytest.skip(f"{template_file.name} needs the temporarily-unavailable {unavailable} bundle") + errors = await validate_flow_execution(client, template_data, template_file.name, logged_in_headers) if errors: error_msg = "\n".join(errors) @@ -122,6 +158,10 @@ class TestStarterProjects: with template_file.open(encoding="utf-8") as f: template_data = json.load(f) + unavailable = _template_unavailable_bundle(template_data) + if unavailable: + pytest.skip(f"{template_file.name} needs the temporarily-unavailable {unavailable} bundle") + errors = [] for node in template_data.get("data", {}).get("nodes", []): node_data = node.get("data", {}) diff --git a/src/frontend/tests/core/features/modelInputComponent.spec.ts b/src/frontend/tests/core/features/modelInputComponent.spec.ts index 0aebdd98ea..4a85326721 100644 --- a/src/frontend/tests/core/features/modelInputComponent.spec.ts +++ b/src/frontend/tests/core/features/modelInputComponent.spec.ts @@ -4,6 +4,13 @@ import { awaitBootstrapTest } from "../../utils/await-bootstrap-test"; import { TEXTS } from "../../utils/constants/texts"; test.describe("ModelInputComponent", () => { + test.beforeEach(() => { + test.skip( + process.platform === "win32", + "Flaky on Windows CI runners: SQLite 'database is locked' during flow teardown cascades into the next test's bootstrap (new-project modal / sidebar never render). The model-provider coverage is OS-agnostic and runs on Linux/macOS", + ); + }); + test( "should display model selector in a node with model input", { tag: ["@release", "@components", "@workspace"] },