144 lines
5.0 KiB
Python
144 lines
5.0 KiB
Python
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>/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"
|