from __future__ import annotations import json from pathlib import Path from beaver.engine.loader import EngineLoader from beaver.foundation.config import BeaverConfig, PluginsConfig from beaver.foundation.utils.file_lock import WorkspaceWriteLock from beaver.memory.skills import SkillLearningStore from beaver.plugins.discovery import discover_plugins from beaver.plugins.skills import PluginManager from beaver.plugins.state import PluginStateStore from beaver.skills.learning.safety import SkillDraftSafetyChecker from beaver.skills.publisher import SkillPublisher from beaver.skills.specs import SkillSpecStore def _write_plugin(root: Path, *, version: str = "1.0.0", body: str = "# Plugin\n\nV1.\n") -> Path: plugin_root = root / "baoyu-comic" skill_root = plugin_root / "skills" / "baoyu-comic" skill_root.mkdir(parents=True, exist_ok=True) (skill_root / "SKILL.md").write_text( "---\nname: baoyu-comic\ndescription: Comic workflow\ntools: []\n---\n\n" + body, encoding="utf-8", ) (plugin_root / "beaver.plugin.json").write_text( json.dumps( { "schema_version": 1, "id": "baoyu-comic", "name": "Baoyu Comic", "version": version, "skills": [{"name": "baoyu-comic", "path": "skills/baoyu-comic"}], } ), encoding="utf-8", ) return plugin_root def _rewrite_plugin(plugin_root: Path, *, version: str, body: str) -> None: manifest_path = plugin_root / "beaver.plugin.json" manifest = json.loads(manifest_path.read_text(encoding="utf-8")) manifest["version"] = version manifest_path.write_text(json.dumps(manifest), encoding="utf-8") (plugin_root / "skills" / "baoyu-comic" / "SKILL.md").write_text( "---\nname: baoyu-comic\ndescription: Comic workflow\ntools: []\n---\n\n" + body, encoding="utf-8", ) def _enable(workspace: Path) -> None: discovery = discover_plugins(workspace, search_paths=[]) store = SkillSpecStore(workspace) PluginManager( workspace=workspace, manifests=discovery.manifests, discovery_errors=discovery.errors, state_store=PluginStateStore(workspace), skill_store=store, learning_store=SkillLearningStore(workspace / "memory" / "skills"), publisher=SkillPublisher(store), safety_checker=SkillDraftSafetyChecker(), write_lock=WorkspaceWriteLock(workspace), ).enable("baoyu-comic") def test_engine_loader_discovers_disabled_plugin_without_mirroring(tmp_path: Path) -> None: workspace = tmp_path / "workspace" _write_plugin(workspace / "plugins") loaded = EngineLoader(workspace=workspace).load() assert "baoyu-comic" not in loaded.skills assert loaded.plugin_manager is not None assert loaded.plugins[0]["id"] == "baoyu-comic" assert loaded.plugins[0]["enabled"] is False def test_engine_loader_syncs_enabled_plugin_updates_before_result_skills(tmp_path: Path) -> None: workspace = tmp_path / "workspace" plugin_root = _write_plugin(workspace / "plugins") _enable(workspace) _rewrite_plugin(plugin_root, version="1.1.0", body="# Plugin\n\nV2.\n") loaded = EngineLoader(workspace=workspace).load() candidates = SkillLearningStore(workspace / "memory" / "skills").list_learning_candidates() assert "baoyu-comic" in loaded.skills assert loaded.plugin_manager is not None assert loaded.plugins[0]["status"] == "update_pending" assert len(candidates) == 1 assert candidates[0].kind == "plugin_skill_update" def test_engine_loader_respects_plugin_auto_sync_config(tmp_path: Path) -> None: workspace = tmp_path / "workspace" plugin_root = _write_plugin(workspace / "plugins") _enable(workspace) _rewrite_plugin(plugin_root, version="1.1.0", body="# Plugin\n\nV2.\n") config = BeaverConfig(plugins=PluginsConfig(auto_sync=False)) loaded = EngineLoader(workspace=workspace, config=config).load() assert loaded.plugin_manager is not None assert SkillLearningStore(workspace / "memory" / "skills").list_learning_candidates() == []