mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-12 12:06:29 +03:00
c49666e29a
* feat(skill): add documents family skill (PDF / Word / Excel / PowerPoint) One family skill for PDF, Word (.docx), Excel (.xlsx), and PowerPoint (.pptx) per the family-skill rule (epub precedent): shared workflow in SKILL.md (scope, content model, template, render, validate, deliver) with per-format load-on-demand references, generation templates per format, a stdlib validation script (--json, structural sanity + render check with graceful degradation), one fixture per format, a unittest suite, six output-quality eval cases spanning all four formats, a human README, the README.md index entry, and regenerated catalogs. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * fix(skill): dispatch PDF renderer args per binary in documents validation The render check passed pdftoppm-only flags (-png/-r/-f/-l) to mutool and ghostscript, which reject them, so a machine with only mutool or gs would false-FAIL valid PDFs. Dispatch per-renderer argument sets (pdftoppm -png; mutool draw -o; gs -sDEVICE=png16m) and cover the dispatch with a unit test. Also: count PDF pages via the /Count page-tree fallback (page objects can hide in compressed ObjStm streams), drop the stale "unsupported input" exit-2 claim from the docstring, and stop labeling skipped files with a FAIL check. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * style(skill): drop redundant local tempfile import in renderer dispatch test Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --------- Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
223 lines
9.0 KiB
Python
223 lines
9.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Tests for documents/scripts/validate-documents.py.
|
|
|
|
Runs standalone (``python3 documents/tests/test_validate_documents.py``) and is
|
|
discovered by scripts/check-artifacts.py's ``unittest`` discovery pass. Every
|
|
test is environment-independent: renderer availability is simulated by
|
|
monkeypatching the module's renderer-discovery functions, never by assuming a
|
|
renderer is or is not installed.
|
|
"""
|
|
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
SCRIPTS_DIR = REPO_ROOT / "documents" / "scripts"
|
|
FIXTURES_DIR = REPO_ROOT / "documents" / "fixtures"
|
|
|
|
SPEC = importlib.util.spec_from_file_location(
|
|
"validate_documents", SCRIPTS_DIR / "validate-documents.py"
|
|
)
|
|
vd = importlib.util.module_from_spec(SPEC)
|
|
SPEC.loader.exec_module(vd)
|
|
|
|
FIXTURES = ["sample.pdf", "sample.docx", "sample.xlsx", "sample.pptx"]
|
|
|
|
|
|
class StructuralValidationTests(unittest.TestCase):
|
|
def test_all_fixtures_pass(self):
|
|
for name in FIXTURES:
|
|
with self.subTest(fixture=name):
|
|
report = vd.validate_files([str(FIXTURES_DIR / name)])
|
|
self.assertEqual(report["status"], "ok")
|
|
self.assertEqual(report["exit_code"], 0)
|
|
self.assertEqual(report["files"][0]["status"], "pass")
|
|
|
|
def test_each_format_detected(self):
|
|
expected = {
|
|
"sample.pdf": "pdf",
|
|
"sample.docx": "docx",
|
|
"sample.xlsx": "xlsx",
|
|
"sample.pptx": "pptx",
|
|
}
|
|
for name, fmt in expected.items():
|
|
with self.subTest(fixture=name):
|
|
report = vd.validate_files([str(FIXTURES_DIR / name)])
|
|
self.assertEqual(report["files"][0]["format"], fmt)
|
|
|
|
def test_json_output_has_status_and_ok(self):
|
|
report = vd.validate_files([str(FIXTURES_DIR / "sample.pdf")])
|
|
self.assertIn("status", report)
|
|
self.assertIn("ok", report)
|
|
self.assertTrue(report["ok"])
|
|
self.assertTrue(json.dumps(report)) # serializable
|
|
|
|
def test_broken_pdf_fails(self):
|
|
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
|
|
tmp.write(b"this is not a pdf at all")
|
|
path = tmp.name
|
|
try:
|
|
report = vd.validate_files([path])
|
|
self.assertEqual(report["status"], "fail")
|
|
self.assertEqual(report["exit_code"], 1)
|
|
self.assertEqual(report["files"][0]["status"], "fail")
|
|
finally:
|
|
os.unlink(path)
|
|
|
|
def test_corrupt_zip_fails(self):
|
|
with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp:
|
|
tmp.write(b"PK\x03\x04 not really a zip")
|
|
path = tmp.name
|
|
try:
|
|
report = vd.validate_files([path])
|
|
self.assertEqual(report["status"], "fail")
|
|
self.assertEqual(report["exit_code"], 1)
|
|
finally:
|
|
os.unlink(path)
|
|
|
|
def test_missing_file_is_error(self):
|
|
report = vd.validate_files([str(REPO_ROOT / "documents" / "fixtures" / "nope.pdf")])
|
|
self.assertEqual(report["status"], "error")
|
|
self.assertEqual(report["exit_code"], 2)
|
|
|
|
def test_unsupported_extension_skipped(self):
|
|
report = vd.validate_files([str(REPO_ROOT / "documents" / "SKILL.md")])
|
|
self.assertEqual(report["files"][0]["status"], "skipped")
|
|
self.assertEqual(report["status"], "ok")
|
|
self.assertEqual(report["exit_code"], 0)
|
|
|
|
|
|
class RenderCheckTests(unittest.TestCase):
|
|
def setUp(self):
|
|
# Force "no renderer installed" for deterministic degradation tests.
|
|
self.orig_pdf = vd.find_pdf_renderer
|
|
self.orig_office = vd.find_office_renderer
|
|
vd.find_pdf_renderer = lambda: None
|
|
vd.find_office_renderer = lambda: None
|
|
|
|
def tearDown(self):
|
|
vd.find_pdf_renderer = self.orig_pdf
|
|
vd.find_office_renderer = self.orig_office
|
|
|
|
def test_render_check_unavailable_without_renderer(self):
|
|
for name in FIXTURES:
|
|
with self.subTest(fixture=name):
|
|
report = vd.validate_files([str(FIXTURES_DIR / name)], render_check=True)
|
|
self.assertEqual(report["status"], "unavailable")
|
|
self.assertEqual(report["exit_code"], 0)
|
|
self.assertEqual(report["files"][0]["render"]["status"], "unavailable")
|
|
|
|
def test_render_check_unavailable_for_unsupported_file(self):
|
|
report = vd.validate_files([str(REPO_ROOT / "documents" / "SKILL.md")], render_check=True)
|
|
self.assertEqual(report["status"], "unavailable")
|
|
self.assertEqual(report["exit_code"], 0)
|
|
|
|
def test_render_ok_with_pdf_renderer(self):
|
|
vd.find_pdf_renderer = lambda: "/usr/bin/env-test-pdftoppm"
|
|
vd.find_office_renderer = lambda: None
|
|
# Renderer discovery is stubbed; render_pdf is stubbed to success so no
|
|
# real binary is required.
|
|
original = vd.render_pdf
|
|
vd.render_pdf = lambda path, tmpdir: {"status": "ok", "renderer": "pdftoppm", "pages": 1}
|
|
try:
|
|
report = vd.validate_files([str(FIXTURES_DIR / "sample.pdf")], render_check=True)
|
|
self.assertEqual(report["status"], "ok")
|
|
self.assertEqual(report["exit_code"], 0)
|
|
self.assertEqual(report["files"][0]["render"]["status"], "ok")
|
|
finally:
|
|
vd.render_pdf = original
|
|
|
|
def test_render_failure_fails_report(self):
|
|
vd.find_pdf_renderer = lambda: "/usr/bin/env-test-pdftoppm"
|
|
original = vd.render_pdf
|
|
vd.render_pdf = lambda path, tmpdir: {"status": "failed", "renderer": "pdftoppm", "reason": "boom"}
|
|
try:
|
|
report = vd.validate_files([str(FIXTURES_DIR / "sample.pdf")], render_check=True)
|
|
self.assertEqual(report["status"], "fail")
|
|
self.assertEqual(report["exit_code"], 1)
|
|
finally:
|
|
vd.render_pdf = original
|
|
|
|
def test_render_pdf_dispatches_renderer_specific_args(self):
|
|
# Each supported PDF renderer has its own CLI; a machine with only
|
|
# mutool or ghostscript (no pdftoppm) must still render correctly.
|
|
captured = {}
|
|
|
|
def fake_run(cmd, capture_output, timeout):
|
|
captured["cmd"] = cmd
|
|
return subprocess.CompletedProcess(cmd, 0, b"", b"")
|
|
|
|
original_run = vd.subprocess.run
|
|
original_find = vd.find_pdf_renderer
|
|
vd.subprocess.run = fake_run
|
|
try:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
vd.find_pdf_renderer = lambda: "/usr/bin/pdftoppm"
|
|
vd.render_pdf(FIXTURES_DIR / "sample.pdf", Path(tmp))
|
|
self.assertIn("-png", captured["cmd"])
|
|
self.assertNotIn("draw", captured["cmd"])
|
|
|
|
vd.find_pdf_renderer = lambda: "/usr/bin/mutool"
|
|
vd.render_pdf(FIXTURES_DIR / "sample.pdf", Path(tmp))
|
|
self.assertIn("draw", captured["cmd"])
|
|
self.assertIn("-o", captured["cmd"])
|
|
|
|
vd.find_pdf_renderer = lambda: "/usr/bin/gs"
|
|
vd.render_pdf(FIXTURES_DIR / "sample.pdf", Path(tmp))
|
|
self.assertIn("-sDEVICE=png16m", captured["cmd"])
|
|
self.assertTrue(any(arg.startswith("-sOutputFile=") for arg in captured["cmd"]))
|
|
finally:
|
|
vd.subprocess.run = original_run
|
|
vd.find_pdf_renderer = original_find
|
|
|
|
|
|
class CliTests(unittest.TestCase):
|
|
def run_cli(self, *args):
|
|
return subprocess.run(
|
|
[sys.executable, str(SCRIPTS_DIR / "validate-documents.py")] + list(args),
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
|
|
def test_help_exits_zero_and_advertises_json(self):
|
|
proc = self.run_cli("--help")
|
|
self.assertEqual(proc.returncode, 0)
|
|
self.assertIn("--json", proc.stdout)
|
|
self.assertIn("--render-check", proc.stdout)
|
|
|
|
def test_cli_json_on_fixture(self):
|
|
proc = self.run_cli("--json", str(FIXTURES_DIR / "sample.xlsx"))
|
|
self.assertEqual(proc.returncode, 0)
|
|
report = json.loads(proc.stdout)
|
|
self.assertEqual(report["status"], "ok")
|
|
|
|
def test_cli_exit_1_on_broken(self):
|
|
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
|
|
tmp.write(b"junk")
|
|
path = tmp.name
|
|
try:
|
|
proc = self.run_cli("--json", path)
|
|
self.assertEqual(proc.returncode, 1)
|
|
finally:
|
|
os.unlink(path)
|
|
|
|
def test_cli_render_check_unavailable_when_no_renderer(self):
|
|
# The CLI uses the real renderer discovery; run it only when we can
|
|
# force the no-renderer path via the module-level stub in-process,
|
|
# which is covered by RenderCheckTests. Here we only assert the CLI
|
|
# accepts the flag and returns 0 or 1 without crashing.
|
|
proc = self.run_cli("--render-check", "--json", str(FIXTURES_DIR / "sample.pdf"))
|
|
self.assertIn(proc.returncode, (0, 1))
|
|
report = json.loads(proc.stdout)
|
|
self.assertIn(report["status"], ("ok", "unavailable", "fail"))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|