"""Tests for ratatoskr.local_agents. Use ``$RATATOSKR_LOCAL_AGENTS`` env-var override + pytest tmp_path to isolate from the operator's real ``~/.config/ratatoskr/local_agents.json``. """ from __future__ import annotations import json from pathlib import Path import pytest from ratatoskr.local_agents import ( LocalAgentEntry, _local_agents_path, add_local_agent, load_local_agents, make_description, remove_local_agent, update_local_agent, ) @pytest.fixture def local_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: """Point $RATATOSKR_LOCAL_AGENTS at a fresh tmp file for the test.""" path = tmp_path / "local_agents.json" monkeypatch.setenv("RATATOSKR_LOCAL_AGENTS", str(path)) return path def _entry( agent_id: str = "ratatoskr:wizard", agent_name: str = "wizard", model: str = "qwen3.6-35-a3b", description: str = "(tier 3) test agent", defined_at: str = "2026-05-25T00:00:00+00:00", ) -> LocalAgentEntry: return LocalAgentEntry( agent_id=agent_id, agent_name=agent_name, model=model, description=description, defined_at=defined_at, ) class TestPathResolution: def test_env_override(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("RATATOSKR_LOCAL_AGENTS", "/tmp/custom-agents.json") assert _local_agents_path() == Path("/tmp/custom-agents.json") def test_xdg_config_home(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("RATATOSKR_LOCAL_AGENTS", raising=False) monkeypatch.setenv("XDG_CONFIG_HOME", "/tmp/xdg-config") assert ( _local_agents_path() == Path("/tmp/xdg-config/ratatoskr/local_agents.json") ) def test_default_home(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("RATATOSKR_LOCAL_AGENTS", raising=False) monkeypatch.delenv("XDG_CONFIG_HOME", raising=False) path = _local_agents_path() assert path == Path.home() / ".config" / "ratatoskr" / "local_agents.json" class TestLoadEmpty: def test_missing_file_returns_empty(self, local_path: Path) -> None: assert not local_path.exists() assert load_local_agents() == [] def test_corrupt_json_returns_empty(self, local_path: Path) -> None: local_path.parent.mkdir(parents=True, exist_ok=True) local_path.write_text("not json at all") assert load_local_agents() == [] def test_wrong_schema_version_returns_empty(self, local_path: Path) -> None: local_path.parent.mkdir(parents=True, exist_ok=True) local_path.write_text(json.dumps({"version": 999, "agents": []})) assert load_local_agents() == [] def test_missing_version_key_returns_empty(self, local_path: Path) -> None: local_path.parent.mkdir(parents=True, exist_ok=True) local_path.write_text(json.dumps({"agents": []})) assert load_local_agents() == [] def test_malformed_row_skipped(self, local_path: Path) -> None: local_path.parent.mkdir(parents=True, exist_ok=True) local_path.write_text( json.dumps( { "version": 1, "agents": [ {"agent_id": "incomplete"}, # missing required fields { "agent_id": "ratatoskr:good", "agent_name": "good", "model": "m", "description": "d", "defined_at": "t", }, ], } ) ) entries = load_local_agents() assert len(entries) == 1 assert entries[0].agent_id == "ratatoskr:good" class TestAdd: def test_add_one(self, local_path: Path) -> None: add_local_agent(_entry()) entries = load_local_agents() assert len(entries) == 1 assert entries[0].agent_id == "ratatoskr:wizard" def test_add_two_different(self, local_path: Path) -> None: add_local_agent(_entry(agent_id="ratatoskr:a", agent_name="a")) add_local_agent(_entry(agent_id="ratatoskr:b", agent_name="b")) ids = {e.agent_id for e in load_local_agents()} assert ids == {"ratatoskr:a", "ratatoskr:b"} def test_add_replaces_same_id(self, local_path: Path) -> None: add_local_agent(_entry(model="old-model")) add_local_agent(_entry(model="new-model")) entries = load_local_agents() assert len(entries) == 1 assert entries[0].model == "new-model" def test_creates_parent_dirs( self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: nested = tmp_path / "deep" / "nested" / "path" / "agents.json" monkeypatch.setenv("RATATOSKR_LOCAL_AGENTS", str(nested)) add_local_agent(_entry()) assert nested.exists() class TestUpdate: def test_update_changes_existing(self, local_path: Path) -> None: add_local_agent(_entry(model="v1")) update_local_agent(_entry(model="v2")) entries = load_local_agents() assert len(entries) == 1 assert entries[0].model == "v2" class TestRemove: def test_remove_existing(self, local_path: Path) -> None: add_local_agent(_entry()) remove_local_agent("ratatoskr:wizard") assert load_local_agents() == [] def test_remove_missing_is_noop(self, local_path: Path) -> None: add_local_agent(_entry()) remove_local_agent("ratatoskr:doesnotexist") assert len(load_local_agents()) == 1 class TestMakeDescription: def test_first_nonempty_line(self) -> None: prompt = "\n\n# IDENTITY\nYou are a test agent..." desc = make_description(prompt) assert desc.startswith("(tier 3) IDENTITY") def test_strips_heading_markers(self) -> None: prompt = "# A nice heading\nMore prompt..." desc = make_description(prompt) assert "(tier 3) A nice heading" == desc def test_truncates_long(self) -> None: prompt = "x" * 200 desc = make_description(prompt) # 80 char cap including the prefix assert len(desc) == 81 # 80 + ellipsis char assert desc.endswith("…") def test_empty_prompt_fallback(self) -> None: desc = make_description("") assert desc == "(tier 3) custom system prompt" def test_whitespace_only_fallback(self) -> None: desc = make_description(" \n\n ") assert desc == "(tier 3) custom system prompt"