from __future__ import annotations import json from pathlib import Path from beaver.plugins.discovery import discover_plugins from beaver.plugins.models import PluginSkillBinding, PluginState from beaver.plugins.state import PluginStateStore def _create_plugin(root: Path, plugin_id: str, *, version: str = "1.0.0") -> Path: plugin_root = root / plugin_id skill_root = plugin_root / "skills" / plugin_id skill_root.mkdir(parents=True) (skill_root / "SKILL.md").write_text(f"# {plugin_id}\n", encoding="utf-8") (plugin_root / "beaver.plugin.json").write_text( json.dumps( { "schema_version": 1, "id": plugin_id, "name": plugin_id.title(), "version": version, "skills": [{"name": plugin_id, "path": f"skills/{plugin_id}"}], } ), encoding="utf-8", ) return plugin_root def test_plugin_state_round_trip_is_atomic(tmp_path: Path) -> None: store = PluginStateStore(tmp_path) store.set_enabled("baoyu-comic", True) store.update_skill_binding( "baoyu-comic", "baoyu-comic", PluginSkillBinding( accepted_upstream_tree_hash="old", observed_upstream_tree_hash="new", accepted_beaver_version="v0001", current_beaver_version="v0002", pending_candidate_id="plugin-update:baoyu-comic:baoyu-comic:new", status="update_pending", ), ) reloaded = PluginStateStore(tmp_path).get_plugin("baoyu-comic") assert reloaded is not None assert reloaded.enabled is True assert reloaded.skills["baoyu-comic"].accepted_upstream_tree_hash == "old" assert not (tmp_path / ".beaver" / "plugins" / "state.json.tmp").exists() def test_plugin_state_preserves_unknown_legacy_fields(tmp_path: Path) -> None: state_path = tmp_path / ".beaver" / "plugins" / "state.json" state_path.parent.mkdir(parents=True) state_path.write_text( json.dumps( { "plugins": { "legacy": { "enabled": True, "installed_version": "1.0.0", "skills": {"legacy": {"status": "synced", "extra": "ignored"}}, "extra": "ignored", } } } ), encoding="utf-8", ) plugin = PluginStateStore(tmp_path).get_plugin("legacy") assert plugin is not None assert plugin.enabled is True assert plugin.skills["legacy"].status == "synced" def test_discover_plugins_scans_workspace_plugins_and_external_roots(tmp_path: Path) -> None: workspace = tmp_path / "workspace" external = tmp_path / "external" _create_plugin(workspace / "plugins", "workspace-plugin") _create_plugin(external, "external-plugin") result = discover_plugins(workspace, search_paths=[external]) assert sorted(result.manifests) == ["external-plugin", "workspace-plugin"] assert result.manifests["workspace-plugin"].display_path == "plugins/workspace-plugin/beaver.plugin.json" assert result.manifests["external-plugin"].display_path == "/external-plugin/beaver.plugin.json" assert result.errors == [] def test_discover_plugins_reports_malformed_manifest_without_crashing(tmp_path: Path) -> None: workspace = tmp_path / "workspace" _create_plugin(workspace / "plugins", "valid") broken = workspace / "plugins" / "broken" broken.mkdir(parents=True) (broken / "beaver.plugin.json").write_text("{not json", encoding="utf-8") result = discover_plugins(workspace, search_paths=[]) assert sorted(result.manifests) == ["valid"] assert len(result.errors) == 1 assert result.errors[0].plugin_id is None assert "broken" in result.errors[0].display_path def test_discover_plugins_reports_duplicate_ids_and_activates_neither(tmp_path: Path) -> None: workspace = tmp_path / "workspace" external = tmp_path / "external" _create_plugin(workspace / "plugins", "dupe") _create_plugin(external, "dupe", version="2.0.0") result = discover_plugins(workspace, search_paths=[external]) assert result.manifests == {} assert len(result.errors) == 2 assert {error.plugin_id for error in result.errors} == {"dupe"} def test_plugin_state_upsert_round_trips_full_state(tmp_path: Path) -> None: store = PluginStateStore(tmp_path) store.upsert_plugin( PluginState( plugin_id="baoyu-comic", enabled=True, updates_paused=True, installed_version="1.2.0", manifest_path="plugins/baoyu-comic/beaver.plugin.json", status="synced", skills={"baoyu-comic": PluginSkillBinding(status="synced")}, ) ) plugin = PluginStateStore(tmp_path).get_plugin("baoyu-comic") assert plugin is not None assert plugin.updates_paused is True assert plugin.installed_version == "1.2.0" assert plugin.manifest_path == "plugins/baoyu-comic/beaver.plugin.json" assert plugin.skills["baoyu-comic"].status == "synced"