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:
rabbitblood 2026-04-16 07:53:03 -07:00
parent 851a6d7bfd
commit 9cdae9afec
7 changed files with 107 additions and 5 deletions

29
.github/workflows/ci.yml vendored Normal file
View 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

View File

@ -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,

View File

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

View File

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

View File

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

View File

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