Every modular workspace template repo (claude-code, hermes, langgraph,
…) was crashing on boot with:
KeyError: "Unknown runtime '<runtime>'. Available: "
Root cause: `molecule_runtime/main.py` and four other modules used
top-level imports like `from adapters import get_adapter` — a monorepo
legacy that resolved when something on sys.path had an `adapters/`
package. Standalone template repos COPY only `adapter.py` (singular) to
/app and don't ship an `adapters/` package, so this import path went
through some side-resolution that left `get_adapter` unable to see the
user's adapter. The ADAPTER_MODULE → import → getattr → issubclass
chain then silently fell through to the discovery branch and reported
"Unknown runtime".
Fix is one-line per file: `from adapters` → `from molecule_runtime.adapters`
in:
- molecule_runtime/main.py:27
- molecule_runtime/a2a_executor.py:44
- molecule_runtime/coordinator.py:20
- molecule_runtime/prompt.py:6
- molecule_runtime/builtin_tools/temporal_workflow.py:417
Tests + CI added so this regression class is caught at PR time, not at
runtime in self-hosters' clusters:
- tests/test_imports.py: parametrised import smoke for every previously
affected module + a grep guard that fails if any future change
reintroduces a top-level `from adapters` / `import adapters` line
- .github/workflows/ci.yml: runs the smoke on every PR (no CI existed
before — the publish workflow only fires on tag push)
Closes #1.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
74 lines
2.8 KiB
Python
74 lines
2.8 KiB
Python
"""Smoke tests for module imports.
|
|
|
|
Catches the class of bug fixed by the absolute-import switch
|
|
(monorepo legacy `from adapters import …` worked when something on
|
|
sys.path had an `adapters/` package; modular template repos that ship
|
|
only `/app/adapter.py` broke). Any future regression to relative-style
|
|
top-level imports gets caught here before publish.
|
|
|
|
These tests run in a clean Python environment with NO `adapters/`
|
|
package on sys.path — exactly the deployment shape consumers see.
|
|
"""
|
|
|
|
import importlib
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
|
|
# Every module that previously had `from adapters import …` or
|
|
# `from adapters.… import …`. Importing any of them must succeed
|
|
# without an `adapters/` package on sys.path.
|
|
MODULES = [
|
|
"molecule_runtime",
|
|
"molecule_runtime.adapters",
|
|
"molecule_runtime.adapters.base",
|
|
"molecule_runtime.main",
|
|
"molecule_runtime.a2a_executor",
|
|
"molecule_runtime.coordinator",
|
|
"molecule_runtime.prompt",
|
|
"molecule_runtime.builtin_tools.temporal_workflow",
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("module_name", MODULES)
|
|
def test_module_imports_without_top_level_adapters_pkg(module_name):
|
|
# Sanity: no top-level `adapters` package shadowing molecule_runtime.adapters.
|
|
assert "adapters" not in sys.modules or sys.modules["adapters"].__name__ != "adapters", (
|
|
"test environment must not have a top-level `adapters` package — "
|
|
"this test catches the regression of importing `from adapters import …` "
|
|
"instead of `from molecule_runtime.adapters import …`"
|
|
)
|
|
mod = importlib.import_module(module_name)
|
|
assert mod is not None
|
|
# Re-import via importlib should be idempotent.
|
|
mod2 = importlib.import_module(module_name)
|
|
assert mod is mod2
|
|
|
|
|
|
def test_get_adapter_resolves_via_absolute_path():
|
|
from molecule_runtime.adapters import get_adapter
|
|
assert callable(get_adapter)
|
|
|
|
|
|
def test_no_top_level_adapters_imports_remain():
|
|
"""Grep guard: keep the import-style invariant explicit so a future
|
|
drive-by change doesn't silently reintroduce `from adapters import`."""
|
|
import os
|
|
pkg_dir = os.path.dirname(importlib.import_module("molecule_runtime").__file__)
|
|
offenders = []
|
|
for dirpath, _, files in os.walk(pkg_dir):
|
|
for fname in files:
|
|
if not fname.endswith(".py"):
|
|
continue
|
|
path = os.path.join(dirpath, fname)
|
|
with open(path, "r", encoding="utf-8") as fh:
|
|
for lineno, line in enumerate(fh, start=1):
|
|
stripped = line.lstrip()
|
|
if stripped.startswith("from adapters") or stripped.startswith("import adapters"):
|
|
offenders.append(f"{path}:{lineno}: {line.rstrip()}")
|
|
assert not offenders, (
|
|
"Top-level `adapters` imports found — must be `from molecule_runtime.adapters …`:\n "
|
|
+ "\n ".join(offenders)
|
|
)
|