"""Unit tests for XML-structured instructions (SPEC3 Section 2)."""
from unittest.mock import patch
from skillport.interfaces.mcp.instructions import (
_escape_xml,
build_xml_instructions,
)
from skillport.shared.config import Config
class TestXmlEscaping:
"""XML character escaping tests."""
def test_escape_ampersand(self):
"""Ampersand is escaped."""
assert _escape_xml("foo & bar") == "foo & bar"
def test_escape_less_than(self):
"""Less-than is escaped."""
assert _escape_xml("a < b") == "a < b"
def test_escape_greater_than(self):
"""Greater-than is escaped."""
assert _escape_xml("a > b") == "a > b"
def test_escape_multiple(self):
"""Multiple special characters are escaped."""
assert _escape_xml("<a & b>") == "<a & b>"
def test_escape_no_special_chars(self):
"""Text without special characters unchanged."""
assert _escape_xml("hello world") == "hello world"
class TestBuildXmlInstructionsStructure:
"""XML structure tests."""
def test_has_skills_system_root(self):
"""Output has <skills_system> root element."""
config = Config(core_skills_mode="none")
with patch(
"skillport.interfaces.mcp.instructions.get_core_skills",
return_value=[],
):
result = build_xml_instructions(config)
assert result.startswith("<skills_system>")
assert result.endswith("</skills_system>")
def test_has_usage_section(self):
"""Output has <usage> section."""
config = Config(core_skills_mode="none")
with patch(
"skillport.interfaces.mcp.instructions.get_core_skills",
return_value=[],
):
result = build_xml_instructions(config)
assert "<usage>" in result
assert "</usage>" in result
def test_usage_contains_workflow(self):
"""Usage section contains workflow instructions."""
config = Config(core_skills_mode="none")
with patch(
"skillport.interfaces.mcp.instructions.get_core_skills",
return_value=[],
):
result = build_xml_instructions(config)
assert "## Workflow" in result
assert "search_skills" in result
assert "load_skill" in result
def test_usage_contains_tools_section(self):
"""Usage section contains tools documentation."""
config = Config(core_skills_mode="none")
with patch(
"skillport.interfaces.mcp.instructions.get_core_skills",
return_value=[],
):
# With read_skill_file registered
result = build_xml_instructions(
config, ["search_skills", "load_skill", "read_skill_file"]
)
assert "## Tools" in result
assert "read_skill_file" in result
class TestBuildXmlInstructionsNoCoreSkills:
"""Tests when no core skills are present."""
def test_no_core_skills_section_when_empty(self):
"""No <core_skills> section when no core skills."""
config = Config(core_skills_mode="none")
with patch(
"skillport.interfaces.mcp.instructions.get_core_skills",
return_value=[],
):
result = build_xml_instructions(config)
assert "<core_skills>" not in result
assert "</core_skills>" not in result
def test_no_skill_elements_when_empty(self):
"""No <skill> elements when no core skills."""
config = Config(core_skills_mode="none")
with patch(
"skillport.interfaces.mcp.instructions.get_core_skills",
return_value=[],
):
result = build_xml_instructions(config)
assert "<skill>" not in result
class TestBuildXmlInstructionsWithCoreSkills:
"""Tests when core skills are present."""
def test_core_skills_section_present(self):
"""<core_skills> section present when core skills exist."""
config = Config(core_skills_mode="auto")
with patch(
"skillport.interfaces.mcp.instructions.get_core_skills",
return_value=[
{"id": "skill-a", "description": "Skill A description"},
],
):
result = build_xml_instructions(config)
assert "<core_skills>" in result
assert "</core_skills>" in result
def test_skill_elements_present(self):
"""<skill> elements present for each core skill."""
config = Config(core_skills_mode="auto")
with patch(
"skillport.interfaces.mcp.instructions.get_core_skills",
return_value=[
{"id": "skill-a", "description": "First skill"},
{"id": "skill-b", "description": "Second skill"},
],
):
result = build_xml_instructions(config)
assert result.count("<skill>") == 2
assert result.count("</skill>") == 2
def test_skill_has_name_element(self):
"""Each skill has <name> element."""
config = Config(core_skills_mode="auto")
with patch(
"skillport.interfaces.mcp.instructions.get_core_skills",
return_value=[
{"id": "my-skill", "description": "My description"},
],
):
result = build_xml_instructions(config)
assert "<name>my-skill</name>" in result
def test_skill_has_description_element(self):
"""Each skill has <description> element."""
config = Config(core_skills_mode="auto")
with patch(
"skillport.interfaces.mcp.instructions.get_core_skills",
return_value=[
{"id": "my-skill", "description": "My description"},
],
):
result = build_xml_instructions(config)
assert "<description>My description</description>" in result
def test_skill_uses_name_when_id_missing(self):
"""Falls back to 'name' key when 'id' is missing."""
config = Config(core_skills_mode="auto")
with patch(
"skillport.interfaces.mcp.instructions.get_core_skills",
return_value=[
{"name": "fallback-name", "description": "Description"},
],
):
result = build_xml_instructions(config)
assert "<name>fallback-name</name>" in result
def test_skill_handles_empty_description(self):
"""Handles skill with empty description."""
config = Config(core_skills_mode="auto")
with patch(
"skillport.interfaces.mcp.instructions.get_core_skills",
return_value=[
{"id": "no-desc", "description": ""},
],
):
result = build_xml_instructions(config)
assert "<name>no-desc</name>" in result
assert "<description></description>" in result
def test_skill_has_location_element_when_path_present(self):
"""Each skill has <location> element when path is provided."""
config = Config(core_skills_mode="auto")
with patch(
"skillport.interfaces.mcp.instructions.get_core_skills",
return_value=[
{
"id": "my-skill",
"description": "My description",
"path": "/home/user/skills/my-skill",
},
],
):
result = build_xml_instructions(config)
assert "<location>/home/user/skills/my-skill/SKILL.md</location>" in result
def test_skill_no_location_element_when_path_missing(self):
"""No <location> element when path is not provided."""
config = Config(core_skills_mode="auto")
with patch(
"skillport.interfaces.mcp.instructions.get_core_skills",
return_value=[
{"id": "my-skill", "description": "My description"},
],
):
result = build_xml_instructions(config)
assert "<location>" not in result
def test_skill_no_location_element_when_path_empty(self):
"""No <location> element when path is empty string."""
config = Config(core_skills_mode="auto")
with patch(
"skillport.interfaces.mcp.instructions.get_core_skills",
return_value=[
{"id": "my-skill", "description": "My description", "path": ""},
],
):
result = build_xml_instructions(config)
assert "<location>" not in result
class TestBuildXmlInstructionsEscaping:
"""Tests for XML escaping in skill data."""
def test_escapes_special_chars_in_name(self):
"""Special characters in skill name are escaped."""
config = Config(core_skills_mode="auto")
with patch(
"skillport.interfaces.mcp.instructions.get_core_skills",
return_value=[
{"id": "foo<bar>", "description": "Test"},
],
):
result = build_xml_instructions(config)
assert "<name>foo<bar></name>" in result
def test_escapes_special_chars_in_description(self):
"""Special characters in description are escaped."""
config = Config(core_skills_mode="auto")
with patch(
"skillport.interfaces.mcp.instructions.get_core_skills",
return_value=[
{"id": "test", "description": "Use & enjoy <skills>"},
],
):
result = build_xml_instructions(config)
assert "<description>Use & enjoy <skills></description>" in result
def test_escapes_special_chars_in_location(self):
"""Special characters in location path are escaped."""
config = Config(core_skills_mode="auto")
with patch(
"skillport.interfaces.mcp.instructions.get_core_skills",
return_value=[
{
"id": "test",
"description": "Test",
"path": "/path/with <special> & chars",
},
],
):
result = build_xml_instructions(config)
assert (
"<location>/path/with <special> & chars/SKILL.md</location>"
in result
)
class TestDynamicInstructionContent:
"""Tests for dynamically generated instruction content."""
def test_instructions_has_workflow_section(self):
"""Generated instructions contain Workflow section."""
config = Config(core_skills_mode="none")
with patch(
"skillport.interfaces.mcp.instructions.get_core_skills",
return_value=[],
):
result = build_xml_instructions(config)
assert "## Workflow" in result
def test_instructions_has_tools_section(self):
"""Generated instructions contain Tools section."""
config = Config(core_skills_mode="none")
with patch(
"skillport.interfaces.mcp.instructions.get_core_skills",
return_value=[],
):
result = build_xml_instructions(config)
assert "## Tools" in result
def test_instructions_has_tips_section(self):
"""Generated instructions contain Tips section."""
config = Config(core_skills_mode="none")
with patch(
"skillport.interfaces.mcp.instructions.get_core_skills",
return_value=[],
):
result = build_xml_instructions(config)
assert "## Tips" in result
def test_instructions_documents_search_skills(self):
"""Generated instructions document search_skills tool."""
config = Config(core_skills_mode="none")
with patch(
"skillport.interfaces.mcp.instructions.get_core_skills",
return_value=[],
):
result = build_xml_instructions(config)
assert "search_skills" in result
def test_instructions_documents_load_skill(self):
"""Generated instructions document load_skill tool."""
config = Config(core_skills_mode="none")
with patch(
"skillport.interfaces.mcp.instructions.get_core_skills",
return_value=[],
):
result = build_xml_instructions(config)
assert "load_skill" in result
def test_instructions_mentions_path_placeholder(self):
"""Generated instructions mention {path} placeholder usage."""
config = Config(core_skills_mode="none")
with patch(
"skillport.interfaces.mcp.instructions.get_core_skills",
return_value=[],
):
result = build_xml_instructions(config)
assert "{path}" in result