Files
yusufkaraaslan--skill_seekers/tests/test_document_skill_builder_hooks.py
wehub-resource-sync 2cab53bc94
Docker Publish / Build and Push Docker Images (map[description:Skill Seekers CLI - Convert documentation to AI skills dockerfile:Dockerfile name:skill-seekers]) (push) Waiting to run
Docker Publish / Build and Push Docker Images (map[description:Skill Seekers MCP Server - 25 tools for AI assistants dockerfile:Dockerfile.mcp name:skill-seekers-mcp]) (push) Waiting to run
Docker Publish / Test Docker Images (push) Blocked by required conditions
Test Vector Database Adaptors / Test chroma Adaptor (push) Waiting to run
Test Vector Database Adaptors / Test faiss Adaptor (push) Waiting to run
Test Vector Database Adaptors / Test qdrant Adaptor (push) Waiting to run
Test Vector Database Adaptors / Test weaviate Adaptor (push) Waiting to run
Test Vector Database Adaptors / Test MCP Vector DB Tools (push) Waiting to run
Tests / Code Quality (Ruff & Mypy) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (macos-latest, 3.11) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (macos-latest, 3.12) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (ubuntu-latest, 3.10) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (ubuntu-latest, 3.11) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (ubuntu-latest, 3.12) (push) Waiting to run
Tests / Tests (push) Blocked by required conditions
Tests / Serial / Integration / E2E Tests (push) Blocked by required conditions
Tests / MCP Server Tests (push) Blocked by required conditions
chore: import upstream snapshot with attribution
2026-07-13 12:46:28 +08:00

160 lines
7.2 KiB
Python

"""Phase 2.1 hook tests — DocumentSkillBuilder variation points.
The golden trees (tests/golden/phase2/*) prove whole-tree byte-identity;
these tests pin the individual hooks added in Phase 2.1 so a future edit
to one hook fails with a precise message instead of a tree-wide diff:
- LOAD_TOTAL_KEY / UNIT_LABEL drive load_extracted_data (pdf/man collapse)
- category_stem + the trailing cat_key parameter of _reference_filename
(chat names multi-category files by category key, single → main.md)
- PATTERN_KEYWORDS / DOC_NOUN drive _format_patterns_from_content
- DOC_NOUN / RANGE_LABEL drive the index.md title and category ranges
- _frontmatter_value quotes SKILL.md frontmatter only for YAML-special
values (safe values stay unquoted so the golden trees hold)
"""
import json
from skill_seekers.cli.chat_scraper import ChatToSkillConverter
from skill_seekers.cli.document_skill_builder import DocumentSkillBuilder
from skill_seekers.cli.html_scraper import HtmlToSkillConverter
from skill_seekers.cli.jupyter_scraper import JupyterToSkillConverter
from skill_seekers.cli.man_scraper import ManPageToSkillConverter
from skill_seekers.cli.pdf_scraper import PDFToSkillConverter
from skill_seekers.cli.pptx_scraper import PptxToSkillConverter
def _chat(tmp_path):
return ChatToSkillConverter(
{"name": "team", "export_path": str(tmp_path), "output_dir": str(tmp_path)}
)
class TestLoadTotalKey:
def test_pdf_loader_reports_total_pages(self, tmp_path, capsys):
json_path = tmp_path / "extracted.json"
json_path.write_text(json.dumps({"pages": [{"page_number": 1}], "total_pages": 7}))
converter = PDFToSkillConverter(
{"name": "guide", "pdf_path": "guide.pdf", "output_dir": str(tmp_path)}
)
assert converter.load_extracted_data(str(json_path)) is True
assert "Loaded 7 pages" in capsys.readouterr().out
def test_man_loader_reports_man_pages(self, tmp_path, capsys):
json_path = tmp_path / "extracted.json"
json_path.write_text(json.dumps({"pages": [], "total_pages": 2}))
converter = ManPageToSkillConverter(
{"name": "tools", "man_names": ["git"], "output_dir": str(tmp_path)}
)
assert converter.load_extracted_data(str(json_path)) is True
assert "Loaded 2 man page(s)" in capsys.readouterr().out
class TestCategoryStemHook:
def test_base_ignores_cat_key_and_uses_source_stem(self, tmp_path):
converter = PDFToSkillConverter(
{"name": "guide", "pdf_path": "guide.pdf", "output_dir": str(tmp_path)}
)
cat = {"title": "Intro", "pages": [{"page_number": 3}, {"page_number": 7}]}
assert converter._reference_filename(cat, 1, 2, "ignored_key") == "guide_p3-p7.md"
def test_chat_multi_category_uses_cat_key(self, tmp_path):
converter = _chat(tmp_path)
cat = {"title": "#eng", "pages": [{"section_number": 1}, {"section_number": 4}]}
assert converter._reference_filename(cat, 1, 3, "eng") == "eng_s1-s4.md"
def test_chat_single_category_collapses_to_main(self, tmp_path):
converter = _chat(tmp_path)
cat = {"title": "#eng", "pages": [{"section_number": 1}]}
# NOT "eng.md" — the single-category export is always main.md.
assert converter._reference_filename(cat, 1, 1, "eng") == "main.md"
def test_chat_empty_category_falls_back_to_section_number(self, tmp_path):
converter = _chat(tmp_path)
assert converter._reference_filename({"title": "x", "pages": []}, 2, 3, "x") == (
"section_02.md"
)
class TestPatternKeywords:
def test_html_extends_shared_keywords(self):
for kw in DocumentSkillBuilder.PATTERN_KEYWORDS:
assert kw in HtmlToSkillConverter.PATTERN_KEYWORDS
assert "changelog" in HtmlToSkillConverter.PATTERN_KEYWORDS
assert "reference" in HtmlToSkillConverter.PATTERN_KEYWORDS
def test_jupyter_extends_shared_keywords(self):
assert "modeling" in JupyterToSkillConverter.PATTERN_KEYWORDS
assert "getting started" in JupyterToSkillConverter.PATTERN_KEYWORDS
def test_keywords_and_noun_shape_the_output(self, tmp_path):
converter = PptxToSkillConverter(
{"name": "deck", "pptx_path": "deck.pptx", "output_dir": str(tmp_path)}
)
converter.extracted_data = {"pages": [{"section_number": 1, "heading": "Agenda for today"}]}
out = converter._format_patterns_from_content()
assert "*Common presentation patterns found:*" in out
assert "**Agenda** (1 sections):" in out
class TestDocNounIndexTitle:
def test_pptx_index_title_and_pdf_range_label(self):
assert PptxToSkillConverter.DOC_NOUN == "presentation"
assert PDFToSkillConverter.RANGE_LABEL == "Pages"
assert DocumentSkillBuilder.RANGE_LABEL == "Sections"
assert DocumentSkillBuilder.DOC_NOUN == "documentation"
class TestFrontmatterQuoting:
def test_safe_values_stay_unquoted(self):
# Byte-identity with the golden trees depends on this.
assert DocumentSkillBuilder._frontmatter_value("golden-word") == "golden-word"
assert (
DocumentSkillBuilder._frontmatter_value("Use when testing the word golden build")
== "Use when testing the word golden build"
)
def test_yaml_special_values_are_json_quoted(self):
assert DocumentSkillBuilder._frontmatter_value("godot: engine") == '"godot: engine"'
assert DocumentSkillBuilder._frontmatter_value("c# basics") == '"c# basics"'
assert DocumentSkillBuilder._frontmatter_value('say "hi"') == '"say \\"hi\\""'
assert DocumentSkillBuilder._frontmatter_value("rock'n'roll") == "\"rock'n'roll\""
assert DocumentSkillBuilder._frontmatter_value(" padded ") == '" padded "'
assert DocumentSkillBuilder._frontmatter_value("two\nlines") == '"two\\nlines"'
def test_skill_md_frontmatter_quotes_hostile_name(self, tmp_path):
converter = HtmlToSkillConverter(
{
"name": "godot: engine",
"html_path": "docs.html",
"description": "Engine docs: 3D and scripting",
"output_dir": str(tmp_path),
}
)
converter.skill_dir = str(tmp_path)
converter.extracted_data = {"pages": [], "metadata": {}}
converter._generate_skill_md({})
lines = (tmp_path / "SKILL.md").read_text(encoding="utf-8").splitlines()
assert lines[0] == "---"
assert lines[1] == 'name: "godot:-engine"'
assert lines[2] == 'description: "Engine docs: 3D and scripting"'
assert lines[3] == "---"
def test_skill_md_frontmatter_keeps_safe_name_unquoted(self, tmp_path):
converter = HtmlToSkillConverter(
{
"name": "godot engine",
"html_path": "docs.html",
"description": "Engine docs",
"output_dir": str(tmp_path),
}
)
converter.skill_dir = str(tmp_path)
converter.extracted_data = {"pages": [], "metadata": {}}
converter._generate_skill_md({})
lines = (tmp_path / "SKILL.md").read_text(encoding="utf-8").splitlines()
assert lines[1] == "name: godot-engine"
assert lines[2] == "description: Engine docs"