mirror of
https://github.com/usestrix/strix.git
synced 2026-08-16 17:27:26 +02:00
235 lines
7.5 KiB
Python
235 lines
7.5 KiB
Python
from collections.abc import Iterator
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
import strix.skills as skills_mod
|
|
from strix.agents.prompt import render_system_prompt
|
|
from strix.skills import (
|
|
get_all_skill_names,
|
|
get_available_skills,
|
|
load_skills,
|
|
register_skill_dir,
|
|
registered_skill_dirs,
|
|
skill_search_dirs,
|
|
validate_requested_skills,
|
|
)
|
|
from strix.utils.resource_paths import get_strix_resource_path
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clear_extra_dirs() -> Iterator[None]:
|
|
original = list(skills_mod._EXTRA_SKILL_DIRS)
|
|
skills_mod._EXTRA_SKILL_DIRS.clear()
|
|
try:
|
|
yield
|
|
finally:
|
|
skills_mod._EXTRA_SKILL_DIRS[:] = original
|
|
|
|
|
|
def _write_skill(root: Path, category: str, name: str, body: str) -> None:
|
|
category_dir = root / category
|
|
category_dir.mkdir(parents=True, exist_ok=True)
|
|
(category_dir / f"{name}.md").write_text(body, encoding="utf-8")
|
|
|
|
|
|
def _write_root_skill(root: Path, name: str, body: str) -> None:
|
|
root.mkdir(parents=True, exist_ok=True)
|
|
(root / f"{name}.md").write_text(body, encoding="utf-8")
|
|
|
|
|
|
def test_no_registration_leaves_builtin_only() -> None:
|
|
assert registered_skill_dirs() == ()
|
|
builtin = get_strix_resource_path("skills")
|
|
assert skill_search_dirs() == (builtin,)
|
|
assert {"nmap", "subfinder"}.issubset(
|
|
{skill["name"] for skill in get_available_skills()["tooling"]}
|
|
)
|
|
|
|
|
|
def test_register_is_idempotent_and_ordered(tmp_path: Path) -> None:
|
|
a = tmp_path / "a"
|
|
b = tmp_path / "b"
|
|
a.mkdir()
|
|
b.mkdir()
|
|
|
|
register_skill_dir(a)
|
|
register_skill_dir(b)
|
|
register_skill_dir(a)
|
|
|
|
# Most recently registered wins → highest precedence first.
|
|
assert registered_skill_dirs() == (b, a)
|
|
|
|
|
|
def test_registered_dir_adds_new_skill(tmp_path: Path) -> None:
|
|
_write_skill(tmp_path, "extra", "widget", "widget body")
|
|
register_skill_dir(tmp_path)
|
|
|
|
assert "widget" in get_all_skill_names()
|
|
assert get_available_skills()["extra"] == [{"name": "widget", "description": ""}]
|
|
assert load_skills(["widget"]) == {"widget": "widget body"}
|
|
|
|
|
|
def test_available_skill_includes_frontmatter_description(tmp_path: Path) -> None:
|
|
_write_skill(
|
|
tmp_path,
|
|
"extra",
|
|
"widget",
|
|
"---\nname: widget\ndescription: Useful widget guidance\n---\nwidget body",
|
|
)
|
|
register_skill_dir(tmp_path)
|
|
|
|
assert get_available_skills()["extra"] == [
|
|
{"name": "widget", "description": "Useful widget guidance"}
|
|
]
|
|
|
|
|
|
def test_available_skill_supports_colon_in_description(tmp_path: Path) -> None:
|
|
_write_skill(
|
|
tmp_path,
|
|
"extra",
|
|
"widget",
|
|
'---\nname: widget\ndescription: "Useful widget: handles YAML"\n---\nwidget body',
|
|
)
|
|
register_skill_dir(tmp_path)
|
|
|
|
assert get_available_skills()["extra"] == [
|
|
{"name": "widget", "description": "Useful widget: handles YAML"}
|
|
]
|
|
|
|
|
|
def test_available_skill_normalizes_quoted_description(tmp_path: Path) -> None:
|
|
_write_skill(
|
|
tmp_path,
|
|
"extra",
|
|
"widget",
|
|
'---\nname: widget\ndescription: "Useful: widget guidance"\n---\nwidget body',
|
|
)
|
|
register_skill_dir(tmp_path)
|
|
|
|
assert get_available_skills()["extra"] == [
|
|
{"name": "widget", "description": "Useful: widget guidance"}
|
|
]
|
|
|
|
|
|
def test_available_skill_normalizes_multiline_descriptions(tmp_path: Path) -> None:
|
|
_write_skill(
|
|
tmp_path,
|
|
"extra",
|
|
"block",
|
|
"---\nname: block\n\ndescription: |\n"
|
|
" First paragraph\n\n Second paragraph\n\n---\nblock body",
|
|
)
|
|
_write_skill(
|
|
tmp_path,
|
|
"extra",
|
|
"plain",
|
|
"---\nname: plain\n\ndescription: First line\n Second line\n\n---\nplain body",
|
|
)
|
|
register_skill_dir(tmp_path)
|
|
|
|
available = {skill["name"]: skill["description"] for skill in get_available_skills()["extra"]}
|
|
assert available == {
|
|
"block": "First paragraph Second paragraph",
|
|
"plain": "First line Second line",
|
|
}
|
|
|
|
|
|
def test_available_skill_supports_block_scalar_trailing_comment(tmp_path: Path) -> None:
|
|
_write_skill(
|
|
tmp_path,
|
|
"extra",
|
|
"commented",
|
|
"---\nname: commented\ndescription: | # paragraph\n"
|
|
" First line\n Second line\n---\ncommented body",
|
|
)
|
|
register_skill_dir(tmp_path)
|
|
|
|
assert get_available_skills()["extra"] == [
|
|
{"name": "commented", "description": "First line Second line"}
|
|
]
|
|
|
|
|
|
def test_malformed_frontmatter_keeps_skill_body(tmp_path: Path) -> None:
|
|
_write_skill(
|
|
tmp_path,
|
|
"extra",
|
|
"broken",
|
|
"---\nname: [broken\ndescription: should be empty\n---\nbroken body",
|
|
)
|
|
register_skill_dir(tmp_path)
|
|
|
|
assert get_available_skills()["extra"] == [{"name": "broken", "description": ""}]
|
|
assert load_skills(["extra/broken"]) == {"broken": "broken body"}
|
|
|
|
|
|
def test_system_prompt_renders_skill_descriptions() -> None:
|
|
prompt = render_system_prompt(scan_mode="quick", is_root=True)
|
|
|
|
assert "- technologies/firebase: Firebase security testing covering" in prompt
|
|
|
|
|
|
def test_system_prompt_omits_empty_skill_description(tmp_path: Path) -> None:
|
|
_write_skill(tmp_path, "extra", "widget", "---\nname: widget\ndescription:\n---\nwidget body")
|
|
register_skill_dir(tmp_path)
|
|
|
|
prompt = render_system_prompt(scan_mode="quick", is_root=True)
|
|
|
|
assert "- extra/widget\n" in prompt
|
|
assert "- extra/widget: " not in prompt
|
|
|
|
|
|
def test_registered_root_skill_is_discoverable_and_valid(tmp_path: Path) -> None:
|
|
_write_root_skill(tmp_path, "widget", "widget body")
|
|
register_skill_dir(tmp_path)
|
|
|
|
assert "widget" in get_all_skill_names()
|
|
assert get_available_skills()["root"] == [{"name": "widget", "description": ""}]
|
|
assert validate_requested_skills(["widget"]) is None
|
|
assert validate_requested_skills(["root/widget"]) is None
|
|
assert load_skills(["widget"]) == {"widget": "widget body"}
|
|
assert load_skills(["root/widget"]) == {"widget": "widget body"}
|
|
|
|
|
|
def test_ambiguous_bare_skill_requires_qualified_name(tmp_path: Path) -> None:
|
|
_write_skill(tmp_path, "alpha", "widget", "alpha body")
|
|
_write_skill(tmp_path, "beta", "widget", "beta body")
|
|
register_skill_dir(tmp_path)
|
|
|
|
assert "widget" in get_all_skill_names()
|
|
assert get_available_skills()["alpha"] == [{"name": "widget", "description": ""}]
|
|
assert get_available_skills()["beta"] == [{"name": "widget", "description": ""}]
|
|
assert validate_requested_skills(["alpha/widget"]) is None
|
|
assert validate_requested_skills(["beta/widget"]) is None
|
|
|
|
error = validate_requested_skills(["widget"])
|
|
assert error is not None
|
|
assert "Ambiguous skill name" in error
|
|
assert "alpha/widget" in error
|
|
assert "beta/widget" in error
|
|
|
|
assert load_skills(["widget"]) == {}
|
|
assert load_skills(["alpha/widget"]) == {"widget": "alpha body"}
|
|
assert load_skills(["beta/widget"]) == {"widget": "beta body"}
|
|
|
|
|
|
def test_registered_dir_overrides_builtin_skill(tmp_path: Path) -> None:
|
|
_write_skill(tmp_path, "coordination", "root_agent", "overridden root agent")
|
|
register_skill_dir(tmp_path)
|
|
|
|
loaded = load_skills(["coordination/root_agent"])
|
|
assert loaded["root_agent"] == "overridden root agent"
|
|
|
|
|
|
def test_builtin_skill_still_loads_when_not_overridden(tmp_path: Path) -> None:
|
|
_write_skill(tmp_path, "extra", "widget", "widget body")
|
|
register_skill_dir(tmp_path)
|
|
|
|
# A packaged skill the registered dir does not shadow still resolves.
|
|
assert load_skills(["scan_modes/deep"]).get("deep")
|
|
|
|
|
|
def test_missing_skill_is_skipped(tmp_path: Path) -> None:
|
|
register_skill_dir(tmp_path)
|
|
assert load_skills(["does_not_exist"]) == {}
|