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
This commit is contained in:
Cristhian Zanforlin Lousa
2026-06-24 13:59:57 -03:00
committed by GitHub
parent cf9cabdd5a
commit d2648b95c7
3 changed files with 95 additions and 1 deletions

View File

@ -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")

View File

@ -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", {})

View File

@ -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"] },