"""Tests for storage/local_skills.py. Covers local-skill CRUD, persistence across reopen, override upsert/remove, and fork-on-revocation (override promoted to a standalone local skill). """ from __future__ import annotations import pytest from skills_learning.models import ( FlowStep, FlowTemplateSkill, KnowledgeSkill, SkillMetadata, ) from storage.local_skills import LOCAL_SOURCE, LocalSkillStore @pytest.fixture def store(tmp_path): return LocalSkillStore(db_path=tmp_path / "local_skills.sqlite3") def _knowledge(name: str, content: str, *, tags=None) -> KnowledgeSkill: return KnowledgeSkill( metadata=SkillMetadata(id="ignored", name=name, kind="knowledge", tags=tags or []), content=content, ) def _flow(name: str, steps, *, parameters=None, tags=None) -> FlowTemplateSkill: return FlowTemplateSkill( metadata=SkillMetadata(id="ignored", name=name, kind="flow_template", tags=tags or []), steps=[FlowStep(tool_name=t, args=a) for t, a in steps], parameters=parameters or {}, ) # --- local skills ----------------------------------------------------------- def test_create_local_assigns_fresh_id_and_source(store): created = store.create_local(_knowledge("Brew", "Boil water")) assert created.id and created.id != "ignored" assert created.metadata.source == LOCAL_SOURCE assert store.get_local(created.id).content == "Boil water" def test_local_skill_persists_across_reopen(tmp_path): path = tmp_path / "local_skills.sqlite3" s1 = LocalSkillStore(db_path=path) created = s1.create_local(_knowledge("Brew", "Boil water")) del s1 s2 = LocalSkillStore(db_path=path) assert s2.get_local(created.id).content == "Boil water" def test_update_local_and_delete_local(store): created = store.create_local(_knowledge("Brew", "Boil water")) store.update_local(KnowledgeSkill(metadata=created.metadata, content="Boil, then steep")) assert store.get_local(created.id).content == "Boil, then steep" assert store.delete_local(created.id) is True assert store.get_local(created.id) is None assert store.delete_local(created.id) is False def test_update_unknown_local_raises(store): with pytest.raises(KeyError): store.update_local(_knowledge("ghost", "x")) def test_list_and_search_local(store): a = store.create_local(_knowledge("Alpha", "a", tags=["x"])) store.create_local(_knowledge("Beta search", "b")) names = [m.name for m in store.list_local()] assert names == ["Alpha", "Beta search"] hits = {m.id for m in store.search_local("search")} assert a.id in hits or True # Beta matches; ensure no crash assert any(m.name == "Beta search" for m in store.search_local("search")) # --- overrides -------------------------------------------------------------- def test_override_upsert_get_remove(store): cloud_id = "cloud-1" assert store.has_override(cloud_id) is False store.upsert_override(cloud_id, _knowledge("Cloud", "overridden content")) assert store.has_override(cloud_id) is True ov = store.get_override(cloud_id) assert ov.metadata.kind == "knowledge" assert ov.content == "overridden content" assert cloud_id in store.list_override_cloud_ids() assert store.remove_override(cloud_id) is True assert store.has_override(cloud_id) is False assert store.remove_override(cloud_id) is False def test_override_replaces(store): store.upsert_override("c1", _knowledge("C", "first")) store.upsert_override("c1", _knowledge("C", "second")) assert store.get_override("c1").content == "second" def test_override_round_trips_flow_template(store): store.upsert_override( "c2", _flow("Flow", [("tap", {"x": 1})], parameters={"x": {"type": "int"}}), ) ov = store.get_override("c2") assert isinstance(ov, FlowTemplateSkill) assert ov.steps[0].tool_name == "tap" assert ov.parameters["x"]["type"] == "int" # --- fork on revocation ----------------------------------------------------- def test_fork_override_promotes_to_local_and_clears_override(store): store.upsert_override("cloud-x", _knowledge("X", "my take")) local_id = store.fork_override_to_local("cloud-x") assert local_id is not None # override gone assert store.has_override("cloud-x") is False # local skill present with new id, origin local forked = store.get_local(local_id) assert forked is not None assert forked.metadata.source == LOCAL_SOURCE assert forked.content == "my take" def test_fork_with_no_override_returns_none(store): assert store.fork_override_to_local("absent") is None