fix: align loader tests with renamed manifest API and strip ticket refs

The merge of feat/extension-validate brought in the LfxCompat / compat
rename, but the loader test fixtures still used LangflowCompat / bundle_api
and failed at the manifest layer. Update conftest.py + test_load_extension.py
to the post-rename API.

Strip 17 LE-XXXX ticket references from loader source files, errors.py
loader-specific comments, and the four loader test docstrings, matching the
convention applied to LE-1014. Descriptive prose preserved.

Promote _BUNDLE_NAME_RE to BUNDLE_NAME_RE on the manifest module so the
loader's orchestrator no longer reaches across modules into a private name.
This commit is contained in:
Eric Hare
2026-05-05 10:06:45 -07:00
parent c9b923a2da
commit c63f84a591
13 changed files with 37 additions and 36 deletions

View File

@ -57,16 +57,17 @@ ERROR_CODES: frozenset[str] = frozenset(
"import-star-disallowed",
"top-level-io-disallowed",
"execute-imports-failed",
# Loader-specific codes (LE-1015)
# Loader-specific codes
"module-import-failed",
"duplicate-component-name",
"duplicate-inline-bundle",
"inline-bundle-name-invalid",
}
)
# NOTE: ``duplicate-distribution`` will be added by LE-1022 (installed-pkg
# discovery) once there is a startup flow + events surface that can actually
# emit it. We hold the line that every registered code must have a producer.
# NOTE: ``duplicate-distribution`` will be added with the installed-package
# discovery flow once there is a startup path + events surface that can
# actually emit it. We hold the line that every registered code must have a
# producer.
# ---------------------------------------------------------------------------

View File

@ -1,4 +1,4 @@
"""Single-Bundle loader for the Langflow Extension System (LE-1015).
"""Single-Bundle loader for the Langflow Extension System.
This package turns a directory tree on disk (an Extension or a loose
LANGFLOW_COMPONENTS_PATH entry) into a list of :class:`LoadedComponent`
@ -45,11 +45,11 @@ Internal layout (all underscore-prefixed; not part of the public surface):
- ``_plugins`` -- manifest-first precedence over ``langflow.plugins``;
installed-distribution discovery primitives.
LE-1022 will reuse ``_plugins.installed_extension_roots`` for the read-only
@official slot at server startup; LE-1018 will reuse ``_orchestrator`` plus
the discovery layer to drive atomic-swap reload. Splitting the loader into
small files now keeps each follow-on ticket touching one banner-section at
a time.
A future installed-package / seed-dir discovery flow will reuse
``_plugins.installed_extension_roots`` for the read-only @official slot at
server startup, and atomic-swap reload will reuse ``_orchestrator`` plus the
discovery layer. Splitting the loader into small files now keeps each
follow-on touching one banner-section at a time.
"""
from lfx.extension.loader._orchestrator import (

View File

@ -1,4 +1,4 @@
"""Component-subclass detection for the extension loader (LE-1015).
"""Component-subclass detection for the extension loader.
After ``_discovery`` imports a bundle module, this module is responsible
for picking the Component subclasses out of the resulting namespace. The

View File

@ -1,4 +1,4 @@
"""Filesystem walk + module import for the extension loader (LE-1015).
"""Filesystem walk + module import for the extension loader.
This module owns the "find files / build module names / actually import"
half of the loader pipeline. It deliberately knows nothing about Component

View File

@ -1,4 +1,4 @@
"""Load orchestration: turn a directory tree into a :class:`LoadResult` (LE-1015).
"""Load orchestration: turn a directory tree into a :class:`LoadResult`.
This module wires the discovery + detection layers together and exposes the
two public entry points the rest of Langflow consumes:
@ -37,7 +37,7 @@ from lfx.extension.loader._types import (
LoadResult,
)
from lfx.extension.manifest import (
_BUNDLE_NAME_RE,
BUNDLE_NAME_RE,
BundleRef,
ExtensionManifest,
load_manifest,
@ -306,7 +306,7 @@ def load_extension(
# Inline-bundle metadata, optionally provided as ``bundle.json`` at the
# bundle's root. Only ``version`` is read in v0; ``name`` is derived from
# the directory name and validated against _BUNDLE_NAME_RE so that the
# the directory name and validated against BUNDLE_NAME_RE so that the
# namespaced ID is well-formed. This is intentionally a tiny shape; full
# manifest support belongs at the @official slot.
_INLINE_BUNDLE_DEFAULT_VERSION = "0.0.0"
@ -382,7 +382,7 @@ def discover_inline_bundles(
continue
result = LoadResult(slot=SLOT_EXTRA, source_path=child)
if not _BUNDLE_NAME_RE.match(name):
if not BUNDLE_NAME_RE.match(name):
result.errors.append(
ExtensionError(
code="inline-bundle-name-invalid",

View File

@ -1,4 +1,4 @@
"""Manifest-first precedence over ``langflow.plugins`` entry-points (LE-1015).
"""Manifest-first precedence over ``langflow.plugins`` entry-points.
This is the bridge between the new manifest-based loader and the legacy
``langflow.plugins`` entry-point system that third parties already use to
@ -9,9 +9,9 @@ to avoid double registration. Non-component entry-points (services,
routes) on the same distribution are unaffected -- the caller's loop is
responsible for that distinction.
LE-1022 will use the same primitives at server startup to drive the
read-only @official slot (installed-package + seed-dir discovery). The
helpers live in their own module so that downstream module gets a stable
The installed-package / seed-dir discovery flow consumes the same
primitives at server startup to drive the read-only @official slot. The
helpers live in their own module so that downstream consumer gets a stable
import surface to reach for.
"""
@ -114,9 +114,9 @@ def installed_extension_roots(
# Two distributions with the same canonical name (rare but possible in
# broken venvs) are resolved by keeping the lexicographically-first
# manifest path for determinism. LE-1022 will surface the conflict as
# a typed warning when it owns the startup-time discovery flow; in this
# primitive we only return the resolved mapping, never an error.
# manifest path for determinism. The startup-time discovery flow will
# surface the conflict as a typed warning when it lands; this primitive
# only returns the resolved mapping, never an error.
found: dict[str, tuple[Path, str]] = {}
for dist in distributions:
manifest_path = _distribution_manifest_path(dist)

View File

@ -1,4 +1,4 @@
"""Public dataclasses and slot constants for the extension loader (LE-1015).
"""Public dataclasses and slot constants for the extension loader.
Kept module-level so consumers (events pipeline, registry, tests) can import
them without paying the cost of dragging in the discovery / detection /
@ -41,10 +41,10 @@ class LoadedComponent:
"""A successfully loaded Component class plus its registry coordinates.
Frozen so callers can place these in sets / dicts and emit them across
the events pipeline (LE-1017) without worrying about mutation.
the events pipeline without worrying about mutation.
The :attr:`namespaced_id` is the canonical address used by saved flows
after the migration table (LE-1020) rewrites legacy references.
after the migration table rewrites legacy references.
"""
extension_id: str

View File

@ -82,7 +82,7 @@ _EXTENSION_ID_RE: re.Pattern[str] = re.compile(r"^[a-z][a-z0-9-]{1,63}$")
"""Extension IDs are lowercase, hyphenated, must start with a letter, 2-64 chars.
Mirrors the npm-package / PyPI normalization rules."""
_BUNDLE_NAME_RE: re.Pattern[str] = re.compile(r"^[a-z][a-z0-9_]{1,63}$")
BUNDLE_NAME_RE: re.Pattern[str] = re.compile(r"^[a-z][a-z0-9_]{1,63}$")
"""Bundle names use snake_case so they can be addressed in the registry as
``ext:<bundle>:<Class>@<slot>`` without quoting."""
@ -191,7 +191,7 @@ class BundleRef(BaseModel):
name: StrictStr = Field(
...,
pattern=_BUNDLE_NAME_RE.pattern,
pattern=BUNDLE_NAME_RE.pattern,
description=(
"Bundle name; addressable as ext:<name>:<Class>@<slot>. "
"Lowercase snake_case, starting with a letter, 2-64 chars."

View File

@ -27,7 +27,7 @@ _BASE_MANIFEST: dict = {
"id": "lfx-pilot",
"version": "1.2.3",
"name": "Pilot Bundle",
"lfx": {"bundle_api": [1]},
"lfx": {"compat": ["1"]},
"bundles": [{"name": "pilot", "path": "components"}],
}
@ -133,7 +133,7 @@ def make_installed_extension(parent: Path, distribution_name: str) -> FakeDist:
"id": distribution_name,
"version": "1.0.0",
"name": distribution_name,
"lfx": {"bundle_api": [1]},
"lfx": {"compat": ["1"]},
"bundles": [{"name": distribution_name.replace("-", "_"), "path": "components"}],
}
manifest_file = pkg_dir / "extension.json"

View File

@ -1,4 +1,4 @@
"""Tests for ``discover_inline_bundles`` (LANGFLOW_COMPONENTS_PATH; LE-1015).
"""Tests for ``discover_inline_bundles`` (LANGFLOW_COMPONENTS_PATH).
Covers the @extra-slot AC items:
- each immediate subfolder of every path is one Bundle

View File

@ -1,4 +1,4 @@
"""Tests for ``load_extension`` -- happy path and failure modes (LE-1015).
"""Tests for ``load_extension`` -- happy path and failure modes.
Covers the AC items for the @official slot:
- single-Bundle happy path + identity tuple population
@ -121,7 +121,7 @@ def test_runtime_multi_bundle_check(tmp_path: Path, monkeypatch: pytest.MonkeyPa
from lfx.extension.manifest import (
BundleRef,
ExtensionManifest,
LangflowCompat,
LfxCompat,
ManifestSource,
)
@ -136,7 +136,7 @@ def test_runtime_multi_bundle_check(tmp_path: Path, monkeypatch: pytest.MonkeyPa
id="lfx-pilot",
version="1.2.3",
name="Pilot",
lfx=LangflowCompat(bundle_api=[1]),
lfx=LfxCompat(compat=["1"]),
bundles=[BundleRef(name="alpha", path="alpha"), BundleRef(name="bravo", path="bravo")],
)
source = ManifestSource.model_construct(manifest=forged, path=tmp_path / "extension.json", kind="extension.json")

View File

@ -1,4 +1,4 @@
"""Tests for manifest-first precedence over ``langflow.plugins`` (LE-1015).
"""Tests for manifest-first precedence over ``langflow.plugins``.
Covers the AC item: "package with manifest + legacy component entry-point
loads its components ONCE via the manifest, not twice." We model "loaded

View File

@ -1,4 +1,4 @@
"""Tests for the loader's public dataclasses (LE-1015).
"""Tests for the loader's public dataclasses.
``LoadedComponent`` and ``LoadResult`` are the values the rest of the
Langflow stack consumes (events pipeline, registry, future reload).