"""Tests for the Cloud API skill sync client and runner enhancements: incremental since_version forwarding and fork-on-revocation (design D3/D9). """ from __future__ import annotations import httpx import pytest from api.skill_sync import ( CloudApiSkillClient, SkillSyncRunner, SyncDelta, ) from skills_learning.models import KnowledgeSkill, SkillMetadata from storage.local_skills import LocalSkillStore from storage.skill_catalog import SkillCatalogStore def _cloud_skill_dict(skill_id: str, name: str, content: str) -> dict: return { "id": skill_id, "name": name, "kind": "knowledge", "description": "d", "tags": [], "revision": 3, "created_at": "2026-01-01T00:00:00+00:00", "updated_at": "2026-01-01T00:00:00+00:00", "content": content, "steps": [], "parameters": {}, } def test_cloud_api_skill_client_parses_revision_to_version(): captured: dict = {} def handler(request: httpx.Request) -> httpx.Response: captured["since"] = request.url.params.get("since_version") captured["auth"] = request.headers.get("authorization") return httpx.Response( 200, json={ "skills": [_cloud_skill_dict("c1", "Cloud One", "body")], "removed_ids": [], "latest_version": 7, "is_full_replace": False, }, ) transport = httpx.MockTransport(handler) client = CloudApiSkillClient( "https://cloud.example/", host_token="host-token", client=httpx.Client(transport=transport), ) delta = client.fetch_entitled_skills("host-1", since_version=5) assert captured["since"] == "5" assert captured["auth"] == "Bearer host-token" assert delta.latest_version == 7 assert delta.is_full_replace is False [skill] = delta.skills assert skill.id == "c1" assert skill.metadata.version == 3 # revision mapped to version assert isinstance(skill, KnowledgeSkill) assert skill.content == "body" class _RecordingClient: def __init__(self, deltas: list[SyncDelta]) -> None: self._deltas = list(deltas) self.seen_since: list[int | None] = [] def fetch_entitled_skills(self, subscription_id, since_version=None): self.seen_since.append(since_version) return self._deltas.pop(0) def test_runner_forwards_since_version_after_first_full_replace(tmp_path): store = SkillCatalogStore(db_path=tmp_path / "skills.sqlite3") deltas = [ SyncDelta( skills=[KnowledgeSkill(metadata=SkillMetadata(id="c1", name="A"), content="x")], removed_ids=[], latest_version=1, is_full_replace=True, ), SyncDelta( skills=[], removed_ids=[], latest_version=1, is_full_replace=False, ), ] client = _RecordingClient(deltas) runner = SkillSyncRunner( store=store, client=client, subscriptions=["host-1"], poll_interval=999.0 ) runner.tick() assert client.seen_since[0] is None # first sync is full runner.tick() assert client.seen_since[1] == 1 # second forwards last version def test_runner_forks_override_on_revocation(tmp_path): store = SkillCatalogStore(db_path=tmp_path / "skills.sqlite3") local = LocalSkillStore(db_path=tmp_path / "local.sqlite3") # Seed the synced store with a cloud skill and a local override for it. store._register_subscription("host-1") cloud_skill = KnowledgeSkill( metadata=SkillMetadata(id="c-cloud", name="Cloud", source="cloud"), content="original", ) store._apply_sync_upsert(cloud_skill, "host-1") local.upsert_override("c-cloud", KnowledgeSkill( metadata=SkillMetadata(id="c-cloud", name="Cloud"), content="my override", )) store._set_subscription_state("host-1", last_synced_version=1) client = _RecordingClient([ SyncDelta(skills=[], removed_ids=["c-cloud"], latest_version=2, is_full_replace=False) ]) runner = SkillSyncRunner( store=store, client=client, subscriptions=["host-1"], poll_interval=999.0, local_store=local, ) runner.tick() # Override forked into a standalone local skill. overrides_after = local.list_overrides() assert overrides_after == [] forked = [s for s in local.list_local() if s.name == "Cloud"] assert len(forked) == 1 # Cloud skill removed from the synced store. assert store.get_skill("c-cloud", {"host-1"}) is None