Merge pull request #472 from Molecule-AI/fix/remove-orphaned-plugin-tests
fix: remove orphaned plugin/adapter tests
This commit is contained in:
commit
3534aa0b5b
@ -1,125 +0,0 @@
|
||||
"""Integration tests: each first-party plugin installs via the registry pipeline.
|
||||
|
||||
Exercises the full flow a workspace runtime goes through at startup:
|
||||
load_plugins() → install_plugins_via_registry() → adaptor.install(ctx)
|
||||
|
||||
For each combination of (plugin, runtime) declared in plugin.yaml, we verify:
|
||||
- the adaptor resolves via the plugin-shipped path (not raw-drop)
|
||||
- skills land in /configs/skills/<skill_name>/
|
||||
- rules/fragments land in /configs/CLAUDE.md
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
_WS_TEMPLATE = Path(__file__).resolve().parents[1]
|
||||
if str(_WS_TEMPLATE) not in sys.path:
|
||||
sys.path.insert(0, str(_WS_TEMPLATE))
|
||||
|
||||
from plugins_registry import AdaptorSource, InstallContext, resolve # noqa: E402
|
||||
|
||||
_REPO_ROOT = _WS_TEMPLATE.parent
|
||||
_PLUGINS_DIR = _REPO_ROOT / "plugins"
|
||||
|
||||
FIRST_PARTY_PLUGINS = ["molecule-dev", "superpowers", "ecc"]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ctx(tmp_path: Path):
|
||||
configs = tmp_path / "configs"
|
||||
configs.mkdir()
|
||||
|
||||
# Simple memory append implementation for the test.
|
||||
def _append(filename: str, content: str) -> None:
|
||||
target = configs / filename
|
||||
existing = target.read_text() if target.exists() else ""
|
||||
marker = content.splitlines()[0] if content else ""
|
||||
if marker and marker in existing:
|
||||
return
|
||||
with open(target, "a") as f:
|
||||
f.write(("\n" if existing and not existing.endswith("\n") else "") + content + "\n")
|
||||
|
||||
def _make(plugin_name: str, runtime: str) -> InstallContext:
|
||||
return InstallContext(
|
||||
configs_dir=configs,
|
||||
workspace_id="ws-test",
|
||||
runtime=runtime,
|
||||
plugin_root=_PLUGINS_DIR / plugin_name,
|
||||
append_to_memory=_append,
|
||||
logger=logging.getLogger(plugin_name),
|
||||
)
|
||||
return _make, configs
|
||||
|
||||
|
||||
@pytest.mark.parametrize("plugin_name", FIRST_PARTY_PLUGINS)
|
||||
def test_plugin_manifest_declares_runtimes(plugin_name: str):
|
||||
"""Each first-party plugin must declare supported runtimes."""
|
||||
manifest = yaml.safe_load((_PLUGINS_DIR / plugin_name / "plugin.yaml").read_text())
|
||||
assert "runtimes" in manifest, f"{plugin_name} missing `runtimes:` in plugin.yaml"
|
||||
assert "claude_code" in manifest["runtimes"]
|
||||
assert "deepagents" in manifest["runtimes"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("plugin_name", FIRST_PARTY_PLUGINS)
|
||||
@pytest.mark.parametrize("runtime", ["claude_code", "deepagents"])
|
||||
def test_plugin_ships_adaptor_file(plugin_name: str, runtime: str):
|
||||
"""Each declared runtime has a physical adaptor file."""
|
||||
adaptor_file = _PLUGINS_DIR / plugin_name / "adapters" / f"{runtime}.py"
|
||||
assert adaptor_file.is_file(), f"{plugin_name} missing adapters/{runtime}.py"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("plugin_name", FIRST_PARTY_PLUGINS)
|
||||
@pytest.mark.parametrize("runtime", ["claude_code", "deepagents"])
|
||||
def test_adaptor_resolves_via_plugin_path(plugin_name: str, runtime: str):
|
||||
"""resolve() must find the plugin-shipped adaptor, not fall back to raw-drop."""
|
||||
plugin_root = _PLUGINS_DIR / plugin_name
|
||||
_, source = resolve(plugin_name, runtime, plugin_root)
|
||||
assert source == AdaptorSource.PLUGIN
|
||||
|
||||
|
||||
@pytest.mark.parametrize("plugin_name,runtime", [
|
||||
(p, r) for p in FIRST_PARTY_PLUGINS for r in ["claude_code", "deepagents"]
|
||||
])
|
||||
async def test_plugin_installs_end_to_end(plugin_name: str, runtime: str, ctx):
|
||||
"""Installing each plugin writes the expected content into /configs."""
|
||||
make_ctx, configs_dir = ctx
|
||||
plugin_root = _PLUGINS_DIR / plugin_name
|
||||
adaptor, source = resolve(plugin_name, runtime, plugin_root)
|
||||
assert source == AdaptorSource.PLUGIN
|
||||
|
||||
result = await adaptor.install(make_ctx(plugin_name, runtime))
|
||||
assert result.plugin_name == plugin_name
|
||||
|
||||
# If the plugin has skills/, each one should now exist under /configs/skills/
|
||||
src_skills = plugin_root / "skills"
|
||||
if src_skills.is_dir():
|
||||
for skill in src_skills.iterdir():
|
||||
if skill.is_dir():
|
||||
assert (configs_dir / "skills" / skill.name).is_dir(), \
|
||||
f"{plugin_name}: skill {skill.name} not copied"
|
||||
|
||||
# If the plugin has rules/, CLAUDE.md should contain the marker.
|
||||
src_rules = plugin_root / "rules"
|
||||
if src_rules.is_dir() and any(p.suffix == ".md" for p in src_rules.iterdir()):
|
||||
claude_md = configs_dir / "CLAUDE.md"
|
||||
assert claude_md.exists(), f"{plugin_name}: CLAUDE.md not created"
|
||||
assert f"# Plugin: {plugin_name} /" in claude_md.read_text()
|
||||
|
||||
|
||||
async def test_install_is_idempotent(ctx):
|
||||
"""Installing molecule-dev twice leaves a single marker, doesn't duplicate."""
|
||||
make_ctx, configs_dir = ctx
|
||||
plugin_root = _PLUGINS_DIR / "molecule-dev"
|
||||
adaptor, _ = resolve("molecule-dev", "claude_code", plugin_root)
|
||||
|
||||
await adaptor.install(make_ctx("molecule-dev", "claude_code"))
|
||||
await adaptor.install(make_ctx("molecule-dev", "claude_code"))
|
||||
|
||||
# At least one skill dir exists; CLAUDE.md has exactly one marker section header.
|
||||
assert (configs_dir / "skills" / "review-loop").is_dir()
|
||||
@ -1,115 +0,0 @@
|
||||
"""Smoke tests for the Hermes adapter.
|
||||
|
||||
Verifies:
|
||||
1. Required files exist under adapters/hermes/
|
||||
2. requirements.txt declares openai>=1.0.0 (primary runtime dep)
|
||||
3. discover_adapters() completes without error
|
||||
4. Other adapters (e.g. langgraph) are unaffected
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERMES_DIR = Path(__file__).parent.parent / "adapters" / "hermes"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixture: isolate adapter cache and sys.modules per test
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_adapter_cache():
|
||||
"""Clear the module-level adapter cache and evict hermes from sys.modules
|
||||
before each test so loader state is fresh, then restore afterwards."""
|
||||
import adapters as pkg
|
||||
|
||||
original_cache = dict(pkg._ADAPTER_CACHE)
|
||||
pkg._ADAPTER_CACHE.clear()
|
||||
|
||||
evicted = {k: sys.modules.pop(k) for k in list(sys.modules)
|
||||
if k == "adapters.hermes" or k.startswith("adapters.hermes.")}
|
||||
yield
|
||||
|
||||
# Restore
|
||||
pkg._ADAPTER_CACHE.clear()
|
||||
pkg._ADAPTER_CACHE.update(original_cache)
|
||||
sys.modules.update(evicted)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Directory layout — PR-1 shell must have exactly these three files
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestHermesShellLayout:
|
||||
|
||||
def test_directory_exists(self):
|
||||
assert HERMES_DIR.is_dir(), "adapters/hermes/ directory is missing"
|
||||
|
||||
@pytest.mark.skip(
|
||||
reason="Dockerfile moved to molecule-ai-workspace-template-hermes standalone repo"
|
||||
)
|
||||
def test_dockerfile_present(self):
|
||||
assert (HERMES_DIR / "Dockerfile").is_file(), "Dockerfile missing from hermes shell"
|
||||
|
||||
def test_init_py_present(self):
|
||||
assert (HERMES_DIR / "__init__.py").is_file(), "__init__.py missing from hermes shell"
|
||||
|
||||
@pytest.mark.skip(
|
||||
reason="requirements.txt moved to molecule-ai-workspace-template-hermes standalone repo"
|
||||
)
|
||||
def test_requirements_txt_present(self):
|
||||
assert (HERMES_DIR / "requirements.txt").is_file(), "requirements.txt missing"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. requirements.txt — primary dependency contract
|
||||
# NOTE: requirements.txt has moved to the standalone template repo.
|
||||
# The source-of-truth checks below are now in that repo's own test suite.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestHermesRequirements:
|
||||
|
||||
@pytest.mark.skip(
|
||||
reason="requirements.txt moved to molecule-ai-workspace-template-hermes standalone repo"
|
||||
)
|
||||
def test_openai_version_pin(self):
|
||||
text = (HERMES_DIR / "requirements.txt").read_text()
|
||||
assert "openai>=1.0.0" in text, (
|
||||
"Expected 'openai>=1.0.0' in requirements.txt — "
|
||||
"the Hermes adapter relies on the OpenAI-compat client for Nous Portal / OpenRouter."
|
||||
)
|
||||
|
||||
@pytest.mark.skip(
|
||||
reason="requirements.txt moved to molecule-ai-workspace-template-hermes standalone repo"
|
||||
)
|
||||
def test_no_heavy_framework_deps(self):
|
||||
"""PR-1 shell must not introduce heavy deps that aren't committed to yet."""
|
||||
text = (HERMES_DIR / "requirements.txt").read_text().lower()
|
||||
heavy = ["langchain", "crewai", "autogen", "langgraph"]
|
||||
found = [dep for dep in heavy if dep in text]
|
||||
assert not found, f"Unexpected heavy deps in hermes requirements.txt: {found}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Loader integration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestHermesLoaderIntegration:
|
||||
|
||||
def test_discover_does_not_raise(self):
|
||||
"""discover_adapters() must complete without raising."""
|
||||
from adapters import discover_adapters
|
||||
result = discover_adapters()
|
||||
assert isinstance(result, dict)
|
||||
|
||||
def test_other_adapters_unaffected(self):
|
||||
"""Hermes registration must not block other adapters from loading.
|
||||
langgraph has no heavy optional deps and should always be discoverable."""
|
||||
from adapters import discover_adapters
|
||||
result = discover_adapters()
|
||||
assert "langgraph" in result, (
|
||||
"langgraph adapter missing — loader may have short-circuited."
|
||||
)
|
||||
@ -1,101 +0,0 @@
|
||||
"""Drift guard: the SDK's AgentskillsAdaptor must stay behaviourally in
|
||||
sync with the runtime's copy.
|
||||
|
||||
The SDK vendors its own copy so plugin authors can unit-test without
|
||||
depending on workspace-template, but a behavioural divergence would be
|
||||
silent — a user fixes a rules-injection bug in one copy and the other
|
||||
goes on emitting the wrong output. This test runs the same install
|
||||
scenario through both copies and asserts the observable side effects
|
||||
are identical (CLAUDE.md contents + skill files on disk + InstallResult
|
||||
payload).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _add_to_path(p: Path) -> None:
|
||||
if str(p) not in sys.path:
|
||||
sys.path.insert(0, str(p))
|
||||
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_add_to_path(_REPO / "workspace-template")
|
||||
_add_to_path(_REPO / "sdk" / "python")
|
||||
|
||||
from plugins_registry.builtins import AgentskillsAdaptor as RuntimeAdaptor # noqa: E402
|
||||
from plugins_registry.protocol import InstallContext as RuntimeCtx # noqa: E402
|
||||
from molecule_plugin.builtins import AgentskillsAdaptor as SDKAdaptor # noqa: E402
|
||||
from molecule_plugin.protocol import InstallContext as SDKCtx # noqa: E402
|
||||
|
||||
|
||||
def _make_plugin(root: Path) -> Path:
|
||||
(root / "rules").mkdir(parents=True)
|
||||
(root / "rules" / "r1.md").write_text("- rule one")
|
||||
(root / "fragment.md").write_text("frag text")
|
||||
(root / "README.md").write_text("skip me")
|
||||
(root / "skills" / "s1").mkdir(parents=True)
|
||||
(root / "skills" / "s1" / "SKILL.md").write_text(
|
||||
"---\nname: s1\ndescription: d\n---\nbody"
|
||||
)
|
||||
# setup.sh proves both adaptors run the hook (drift guard for the
|
||||
# plugin-owned dependency-install step). The marker file lets the
|
||||
# test assert the script actually executed.
|
||||
setup = root / "setup.sh"
|
||||
setup.write_text(
|
||||
'#!/bin/bash\nset -e\ntouch "$CONFIGS_DIR/setup-ran-marker"\n'
|
||||
)
|
||||
setup.chmod(0o755)
|
||||
return root
|
||||
|
||||
|
||||
def _memory_sink(store: dict):
|
||||
def _append(filename: str, content: str) -> None:
|
||||
store.setdefault(filename, "")
|
||||
store[filename] = (store[filename] + ("\n" if store[filename] else "") + content + "\n")
|
||||
return _append
|
||||
|
||||
|
||||
async def _install(adaptor_cls, ctx_cls, plugin: Path, configs: Path) -> tuple[list[str], dict]:
|
||||
mem: dict = {}
|
||||
ctx = ctx_cls(
|
||||
configs_dir=configs,
|
||||
workspace_id="ws",
|
||||
runtime="claude_code",
|
||||
plugin_root=plugin,
|
||||
append_to_memory=_memory_sink(mem),
|
||||
logger=logging.getLogger("drift"),
|
||||
)
|
||||
result = await adaptor_cls("my-plugin", "claude_code").install(ctx)
|
||||
return sorted(result.files_written), mem
|
||||
|
||||
|
||||
async def test_sdk_and_runtime_produce_identical_side_effects(tmp_path: Path):
|
||||
"""SDK.install() and runtime.install() must yield byte-identical
|
||||
memory text and skill-file placement for the same input plugin."""
|
||||
plugin_runtime = _make_plugin(tmp_path / "plugin-a")
|
||||
plugin_sdk = _make_plugin(tmp_path / "plugin-b")
|
||||
configs_runtime = tmp_path / "configs-a"
|
||||
configs_runtime.mkdir()
|
||||
configs_sdk = tmp_path / "configs-b"
|
||||
configs_sdk.mkdir()
|
||||
|
||||
rt_files, rt_mem = await _install(RuntimeAdaptor, RuntimeCtx, plugin_runtime, configs_runtime)
|
||||
sdk_files, sdk_mem = await _install(SDKAdaptor, SDKCtx, plugin_sdk, configs_sdk)
|
||||
|
||||
assert rt_files == sdk_files, "copied-files lists diverge"
|
||||
assert rt_mem == sdk_mem, (
|
||||
"CLAUDE.md contents diverge between SDK and runtime AgentskillsAdaptor"
|
||||
)
|
||||
# Both adaptors must run setup.sh — the marker file proves it. If only
|
||||
# one side runs the hook, plugin authors get false-pass unit tests
|
||||
# against the SDK while production behaves differently.
|
||||
assert (configs_runtime / "setup-ran-marker").is_file(), (
|
||||
"runtime AgentskillsAdaptor did not execute setup.sh"
|
||||
)
|
||||
assert (configs_sdk / "setup-ran-marker").is_file(), (
|
||||
"SDK AgentskillsAdaptor did not execute setup.sh — drift from runtime"
|
||||
)
|
||||
Loading…
Reference in New Issue
Block a user