Newer
Older
navi-1 / tests / unit / tools / test_skills.py
"""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