"""Tests for the skills/protocols document format."""
import pytest
from navi.skills.models import (
SkillError,
content_hash,
parse_document,
render_for_agent,
)
SKILL = """---
kind: skill
name: Разбор логов
description: Как быстро найти причину в большом логе.
area: workflows
tags: [logs, debugging]
triggers:
- "нужно найти ошибку в логах"
---
Сначала грепай по уровню ERROR, затем сужай по времени.
"""
PROTOCOL = """---
kind: protocol
name: Выключение инфраструктуры
description: Останавливает сервисы в порядке зависимостей.
area: infrastructure
tags: [power, shutdown]
steps:
- Остановить приложения
- Остановить БД
post_checks:
- Прикладных юнитов не осталось
on_deviation: Вернуться к предыдущему шагу и доложить.
on_blocked: Остановить протокол и доложить.
on_already_done: Проверить статус и отметить шаг.
---
Известные ловушки в конце документа.
"""
class TestParse:
def test_skill(self):
doc = parse_document(SKILL)
assert doc.kind == "skill"
assert doc.name == "Разбор логов"
assert doc.area == "workflows"
assert doc.tags == ["logs", "debugging"]
assert doc.triggers == ["нужно найти ошибку в логах"]
assert "грепай" in doc.body
assert doc.steps == []
def test_protocol(self):
doc = parse_document(PROTOCOL)
assert doc.kind == "protocol"
assert doc.steps == ["Остановить приложения", "Остановить БД"]
assert doc.post_checks == ["Прикладных юнитов не осталось"]
assert doc.on_deviation.startswith("Вернуться")
assert "ловушки" in doc.body
def test_kind_override_matches_frontmatter(self):
assert parse_document(PROTOCOL, kind="protocol").kind == "protocol"
def test_kind_mismatch_is_rejected(self):
with pytest.raises(SkillError, match="front matter says kind"):
parse_document(PROTOCOL, kind="skill")
def test_missing_frontmatter(self):
with pytest.raises(SkillError, match="front matter"):
parse_document("just text")
def test_invalid_yaml(self):
with pytest.raises(SkillError, match="invalid YAML"):
parse_document("---\nname: [unclosed\n---\nbody")
def test_kind_required(self):
with pytest.raises(SkillError, match="'kind' is required"):
parse_document("---\nname: x\n---\nbody")
def test_bad_kind(self):
with pytest.raises(SkillError, match="'kind' must be one of"):
parse_document("---\nkind: method\nname: x\n---\nbody")
def test_name_required(self):
with pytest.raises(SkillError, match="'name' is required"):
parse_document("---\nkind: skill\n---\nbody")
def test_skill_needs_a_body(self):
with pytest.raises(SkillError, match="needs a body"):
parse_document("---\nkind: skill\nname: x\n---\n")
def test_bad_area(self):
with pytest.raises(SkillError, match="'area' must be one of"):
parse_document("---\nkind: skill\nname: x\narea: garage\n---\nbody")
def test_protocol_missing_escape_hatches_lists_every_missing_key(self):
doc = "---\nkind: protocol\nname: x\n---\nbody"
with pytest.raises(SkillError) as exc:
parse_document(doc)
message = str(exc.value)
for key in ("steps", "post_checks", "on_deviation", "on_blocked", "on_already_done"):
assert key in message
def test_scalar_trigger_becomes_list(self):
doc = parse_document(
"---\nkind: skill\nname: x\ntriggers: одна причина\n---\nbody"
)
assert doc.triggers == ["одна причина"]
def test_unknown_keys_are_kept_as_extra(self):
doc = parse_document("---\nkind: skill\nname: x\nowner_team: ops\n---\nbody")
assert doc.extra == {"owner_team": "ops"}
assert doc.frontmatter()["owner_team"] == "ops"
def test_content_hash_is_stable(self):
assert content_hash(SKILL) == content_hash(SKILL)
assert content_hash(SKILL) != content_hash(PROTOCOL)
class TestRender:
def test_skill_render(self):
text = render_for_agent(parse_document(SKILL), entry_id="abc", version=2)
assert text.startswith("SKILL: Разбор логов (v2)")
assert "area: workflows" in text
assert "use when:" in text
assert "грепай" in text
assert "id: abc" in text
def test_protocol_render_has_steps_and_hatches(self):
text = render_for_agent(parse_document(PROTOCOL), entry_id="p1", version=1)
assert text.startswith("PROTOCOL: Выключение инфраструктуры")
assert "1. Остановить приложения" in text
assert "2. Остановить БД" in text
assert "If the run goes off plan:" in text
assert "If the protocol cannot be completed:" in text
assert "If a step is already done:" in text