fix: switch top-level from adapters import to absolute imports (#1)
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>
This commit is contained in:
parent
851a6d7bfd
commit
9cdae9afec
29
.github/workflows/ci.yml
vendored
Normal file
29
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install package + test deps
|
||||
run: |
|
||||
pip install -e .
|
||||
pip install pytest
|
||||
|
||||
- name: Run import smoke tests
|
||||
# Critical: these tests run in an environment with NO top-level
|
||||
# `adapters/` package on sys.path. They catch the regression that
|
||||
# broke every modular workspace template repo before the absolute-
|
||||
# import fix. Do not weaken — the failure mode (silent fallthrough
|
||||
# in get_adapter → "Unknown runtime") is hard to debug at runtime.
|
||||
run: pytest tests/ -v
|
||||
@ -41,7 +41,7 @@ from a2a.server.events import EventQueue
|
||||
from a2a.server.tasks import TaskUpdater
|
||||
from a2a.types import Part, TextPart
|
||||
from a2a.utils import new_agent_text_message
|
||||
from adapters.shared_runtime import (
|
||||
from molecule_runtime.adapters.shared_runtime import (
|
||||
extract_history as _extract_history,
|
||||
extract_message_text,
|
||||
brief_task,
|
||||
|
||||
@ -414,7 +414,7 @@ class TemporalWorkflowWrapper:
|
||||
|
||||
# Build serialisable AgentTaskInput
|
||||
try:
|
||||
from adapters.shared_runtime import (
|
||||
from molecule_runtime.adapters.shared_runtime import (
|
||||
extract_history as _extract_history,
|
||||
extract_message_text,
|
||||
)
|
||||
|
||||
@ -17,7 +17,7 @@ import os
|
||||
|
||||
import httpx
|
||||
from langchain_core.tools import tool
|
||||
from adapters.shared_runtime import build_peer_section
|
||||
from molecule_runtime.adapters.shared_runtime import build_peer_section
|
||||
from policies.routing import build_team_routing_payload
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -24,7 +24,7 @@ from a2a.server.request_handlers import DefaultRequestHandler
|
||||
from a2a.server.tasks import InMemoryTaskStore
|
||||
from a2a.types import AgentCard, AgentCapabilities, AgentSkill
|
||||
|
||||
from adapters import get_adapter, AdapterConfig
|
||||
from molecule_runtime.adapters import get_adapter, AdapterConfig
|
||||
from config import load_config
|
||||
from heartbeat import HeartbeatLoop
|
||||
from preflight import run_preflight, render_preflight_report
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
from pathlib import Path
|
||||
|
||||
from skill_loader.loader import LoadedSkill
|
||||
from adapters.shared_runtime import build_peer_section
|
||||
from molecule_runtime.adapters.shared_runtime import build_peer_section
|
||||
|
||||
DEFAULT_MEMORY_SNAPSHOT_FILES = ("MEMORY.md", "USER.md")
|
||||
|
||||
|
||||
73
tests/test_imports.py
Normal file
73
tests/test_imports.py
Normal file
@ -0,0 +1,73 @@
|
||||
"""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)
|
||||
)
|
||||
Loading…
Reference in New Issue
Block a user