"""Tests for the `skills` tool (search / load) against a fake catalog."""
from datetime import datetime, timezone
import pytest
from navi.skills.models import parse_document
from navi.skills.store import SkillEntry
from navi.tools._internal.base import ToolContext
from navi.tools.skills import SkillTool
SKILL_DOC = """---
kind: skill
name: Разбор логов
description: Быстрый поиск причины в логах.
area: workflows
tags: [logs]
triggers: [нужно найти ошибку]
---
Грепай ERROR, потом сужай по времени.
"""
PROTOCOL_DOC = """---
kind: protocol
name: Выключение
description: Порядок выключения.
area: infrastructure
steps: [Остановить сервисы, Отмонтировать диски]
post_checks: [Юнитов не осталось]
on_deviation: Остановиться и доложить.
on_blocked: Прервать и доложить.
on_already_done: Проверить и пропустить.
---
"""
def _entry(content: str, **kw) -> SkillEntry:
doc = parse_document(content)
base = dict(
id=kw.pop("id", doc.kind + "-1"),
kind=doc.kind,
name=doc.name,
description=doc.description,
triggers=doc.triggers,
tags=doc.tags,
area=doc.area,
visibility=kw.pop("visibility", "personal"),
owner_id=kw.pop("owner_id", "u1"),
content=content,
version=kw.pop("version", 1),
content_hash="hash",
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
)
base.update(kw)
return SkillEntry(**base)
class FakeStore:
def __init__(self, entries):
self.entries = entries
self.calls = []
async def list(self, *, user_id, kind=None, query=None, tag=None, area=None, limit=100):
self.calls.append(("list", kind, query, tag, area))
out = []
for e in self.entries:
if e.owner_id != user_id and e.visibility != "shared":
continue
if kind and e.kind != kind:
continue
if tag and tag not in e.tags:
continue
if area and e.area != area:
continue
if query and query.lower() not in (e.name + " " + e.description).lower():
continue
out.append(e)
return out
async def find(self, *, user_id, name=None, kind=None, skill_id=None):
self.calls.append(("find", skill_id, name, kind))
for e in self.entries:
if e.owner_id != user_id and e.visibility != "shared":
continue
if skill_id and e.id != skill_id:
continue
if name and e.name.lower() != name.lower():
continue
if kind and e.kind != kind:
continue
return e
return None
@pytest.fixture
def ctx():
return ToolContext(session_id="s", user_id="u1")
async def test_search_lists_both_kinds(ctx):
tool = SkillTool(FakeStore([_entry(SKILL_DOC), _entry(PROTOCOL_DOC)]))
result = await tool.execute({"op": "search"}, ctx)
assert result.success
assert "2 matching entries" in result.output
assert "[skill] Разбор логов" in result.output
assert "[protocol] Выключение" in result.output
assert "id=skill-1" in result.output
async def test_search_filters(ctx):
store = FakeStore([_entry(SKILL_DOC), _entry(PROTOCOL_DOC)])
tool = SkillTool(store)
result = await tool.execute({"op": "search", "kind": "protocol", "area": "infrastructure"}, ctx)
assert "[protocol]" in result.output
assert "[skill]" not in result.output
assert store.calls[-1] == ("list", "protocol", None, None, "infrastructure")
async def test_search_empty_explains_what_to_try(ctx):
tool = SkillTool(FakeStore([]))
result = await tool.execute({"op": "search", "query": "nothing"}, ctx)
assert result.success
assert "No matching skills or protocols" in result.output
async def test_load_by_id_renders_protocol(ctx):
tool = SkillTool(FakeStore([_entry(PROTOCOL_DOC)]))
result = await tool.execute({"op": "load", "id": "protocol-1"}, ctx)
assert result.success
assert result.output.startswith("PROTOCOL: Выключение")
assert "1. Остановить сервисы" in result.output
assert "If the run goes off plan" in result.output
assert result.metadata["kind"] == "protocol"
async def test_load_by_name(ctx):
tool = SkillTool(FakeStore([_entry(SKILL_DOC)]))
result = await tool.execute({"op": "load", "name": "разбор логов"}, ctx)
assert result.success
assert "Грепай ERROR" in result.output
async def test_load_missing_points_at_search(ctx):
tool = SkillTool(FakeStore([]))
result = await tool.execute({"op": "load", "id": "nope"}, ctx)
assert not result.success
assert "Search first" in result.error
async def test_load_requires_id_or_name(ctx):
tool = SkillTool(FakeStore([]))
result = await tool.execute({"op": "load"}, ctx)
assert not result.success
assert "'id' or 'name'" in result.error
async def test_unknown_op(ctx):
tool = SkillTool(FakeStore([]))
result = await tool.execute({"op": "delete"}, ctx)
assert not result.success
assert "Unknown op" in result.error