"""Tests for the host-agent skill sync wiring (ยง7.2/7.4).""" from __future__ import annotations import httpx import pytest from host_agent.config import HostAgentConfig from host_agent.skill_sync import HostAgentSkillSync def _config(tmp_path) -> HostAgentConfig: return HostAgentConfig( control_plane_url="https://cloud.example", host_id="host-1", token="host-token", identity_path=tmp_path / "identity.json", skill_sync_interval_seconds=0.01, ) def test_skill_sync_pulls_delta_and_reports_inventory(tmp_path): seen_inventory = {"host": None, "body": None} def handler(request: httpx.Request) -> httpx.Response: path = request.url.path if path.endswith("/skills/sync"): return httpx.Response( 200, json={ "skills": [ { "id": "c1", "name": "Cloud Skill", "kind": "knowledge", "description": "", "tags": [], "revision": 1, "created_at": "2026-01-01T00:00:00+00:00", "updated_at": "2026-01-01T00:00:00+00:00", "content": "body", "steps": [], "parameters": {}, } ], "removed_ids": [], "latest_version": 1, "is_full_replace": True, }, ) if path.endswith("/skills/inventory"): seen_inventory["host"] = request.url.path.split("/")[4] seen_inventory["body"] = request.read() return httpx.Response(204) return httpx.Response(404) sync = HostAgentSkillSync( _config(tmp_path), http_client=httpx.Client(transport=httpx.MockTransport(handler)), ) # One manual tick applies the cloud skill to the synced store. outcomes = sync.runner.tick() assert outcomes["host-1"].success is True visible = sync.synced_store.list_skills({"host-1"}) assert [m.name for m in visible] == ["Cloud Skill"] # Inventory report of an authored local skill is best-effort and payload-shaped. from skills_learning.models import KnowledgeSkill, SkillMetadata sync.local_store.create_local( KnowledgeSkill(metadata=SkillMetadata(name="Local Note", kind="knowledge"), content="x") ) sync.report_inventory_once() assert seen_inventory["host"] == "host-1" assert b"Local Note" in seen_inventory["body"] # start/stop lifecycle does not raise. sync.start() sync.stop() def test_skill_sync_inventory_failure_is_isolated(tmp_path): def handler(request: httpx.Request) -> httpx.Response: if request.url.path.endswith("/skills/inventory"): return httpx.Response(500) return httpx.Response(200, json={"skills": [], "removed_ids": [], "latest_version": 0, "is_full_replace": True}) sync = HostAgentSkillSync( _config(tmp_path), http_client=httpx.Client(transport=httpx.MockTransport(handler)), ) # A failed inventory report must not raise. sync.report_inventory_once() sync.client.close()