177 lines
5.5 KiB
Python
177 lines
5.5 KiB
Python
"""docx skill — load, eligibility, and create→inspect round-trip."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from opensquilla.skills.eligibility import EligibilityContext, check_eligibility
|
|
from opensquilla.skills.loader import SkillLoader
|
|
from opensquilla.skills.meta.executors.skill_exec import run_skill_exec_step
|
|
from opensquilla.skills.meta.types import MetaStep
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
BUNDLED = ROOT / "src" / "opensquilla" / "skills" / "bundled"
|
|
DOCX_DIR = BUNDLED / "docx"
|
|
SCRIPTS = DOCX_DIR / "scripts"
|
|
|
|
|
|
def _spec_to_loader() -> object:
|
|
return SkillLoader(bundled_dir=BUNDLED).get_by_name("docx")
|
|
|
|
|
|
def test_skill_loads() -> None:
|
|
spec = _spec_to_loader()
|
|
assert spec is not None
|
|
assert spec.name == "docx"
|
|
assert spec.metadata is not None
|
|
assert spec.provenance.origin == "clawhub-mit0"
|
|
assert spec.provenance.license == "MIT-0"
|
|
assert spec.entrypoint is not None
|
|
|
|
|
|
def test_eligibility_with_python_present(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(
|
|
"opensquilla.skills.eligibility.shutil.which",
|
|
lambda name: "/usr/bin/python3" if name in {"python", "python3"} else None,
|
|
)
|
|
spec = _spec_to_loader()
|
|
assert spec is not None
|
|
assert check_eligibility(spec, EligibilityContext.auto())
|
|
|
|
|
|
def test_eligibility_without_python(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(
|
|
"opensquilla.skills.eligibility.shutil.which",
|
|
lambda name: None,
|
|
)
|
|
spec = _spec_to_loader()
|
|
assert spec is not None
|
|
assert not check_eligibility(spec, EligibilityContext.auto())
|
|
|
|
|
|
def test_create_then_inspect_round_trip(tmp_path: Path) -> None:
|
|
sys.path.insert(0, str(SCRIPTS))
|
|
try:
|
|
import create_docx # type: ignore[import-not-found]
|
|
import inspect_docx # type: ignore[import-not-found]
|
|
finally:
|
|
sys.path.pop(0)
|
|
|
|
spec = {
|
|
"metadata": {"title": "Round-trip", "author": "Tester"},
|
|
"body": [
|
|
{"kind": "heading", "level": 1, "text": "Hello"},
|
|
{"kind": "paragraph", "text": "World."},
|
|
{"kind": "table", "rows": [["A", "B"], ["1", "2"]]},
|
|
],
|
|
}
|
|
out_path = tmp_path / "out.docx"
|
|
doc = create_docx.build(spec)
|
|
doc.save(str(out_path))
|
|
assert out_path.exists()
|
|
|
|
inspected = inspect_docx.inspect(out_path)
|
|
assert inspected["sections"] >= 1
|
|
texts = [p["text"] for p in inspected["paragraphs"]]
|
|
assert "Hello" in texts
|
|
assert "World." in texts
|
|
assert inspected["tables"] and inspected["tables"][0][0] == ["A", "B"]
|
|
assert inspected["has_tracked_changes"] is False
|
|
|
|
|
|
def test_edit_replace_text(tmp_path: Path) -> None:
|
|
sys.path.insert(0, str(SCRIPTS))
|
|
try:
|
|
import create_docx # type: ignore[import-not-found]
|
|
import edit_docx # type: ignore[import-not-found]
|
|
import inspect_docx # type: ignore[import-not-found]
|
|
finally:
|
|
sys.path.pop(0)
|
|
|
|
src = tmp_path / "src.docx"
|
|
create_docx.build(
|
|
{"body": [{"kind": "paragraph", "text": "Hello {{NAME}}, welcome."}]}
|
|
).save(str(src))
|
|
|
|
from docx import Document
|
|
|
|
doc = Document(str(src))
|
|
ops = [{"op": "replace_text", "find": "{{NAME}}", "with": "Wei"}]
|
|
edit_docx.apply_ops(doc, ops)
|
|
out = tmp_path / "out.docx"
|
|
doc.save(str(out))
|
|
|
|
inspected = inspect_docx.inspect(out)
|
|
text = " ".join(p["text"] for p in inspected["paragraphs"])
|
|
assert "{{NAME}}" not in text
|
|
assert "Wei" in text
|
|
|
|
|
|
def test_inspect_cli_outputs_json(tmp_path: Path) -> None:
|
|
sys.path.insert(0, str(SCRIPTS))
|
|
try:
|
|
import create_docx # type: ignore[import-not-found]
|
|
import inspect_docx # type: ignore[import-not-found]
|
|
finally:
|
|
sys.path.pop(0)
|
|
|
|
src = tmp_path / "src.docx"
|
|
create_docx.build({"body": [{"kind": "paragraph", "text": "x"}]}).save(str(src))
|
|
|
|
payload = inspect_docx.inspect(src)
|
|
encoded = json.dumps(payload, ensure_ascii=False)
|
|
assert "paragraphs" in encoded
|
|
assert "tables" in encoded
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_skill_exec_exports_markdown_docx(tmp_path: Path) -> None:
|
|
sys.path.insert(0, str(SCRIPTS))
|
|
try:
|
|
import inspect_docx # type: ignore[import-not-found]
|
|
finally:
|
|
sys.path.pop(0)
|
|
|
|
loader = SkillLoader(
|
|
bundled_dir=BUNDLED,
|
|
snapshot_path=tmp_path / "skills_snapshot.json",
|
|
)
|
|
loader.invalidate_cache()
|
|
out_path = tmp_path / "competitive-intel.docx"
|
|
step = MetaStep(
|
|
id="export_docx",
|
|
kind="skill_exec",
|
|
skill="docx",
|
|
with_args={
|
|
"markdown": (
|
|
"# Competitive intel brief\n\n"
|
|
"Acme has a new hiring signal.\n\n"
|
|
"| Account | Signal |\n"
|
|
"|---|---|\n"
|
|
"| Acme | hiring |\n"
|
|
),
|
|
"output_path": str(out_path),
|
|
},
|
|
)
|
|
|
|
result = await run_skill_exec_step(
|
|
step,
|
|
"docx",
|
|
{"collected": {"intel_clarify": {"export_docx": "YES"}}},
|
|
{"intel_brief_audit": "Competitive intel brief"},
|
|
skill_loader=loader,
|
|
workspace_dir=str(tmp_path),
|
|
)
|
|
|
|
assert result == str(out_path)
|
|
inspected = inspect_docx.inspect(out_path)
|
|
texts = [p["text"] for p in inspected["paragraphs"]]
|
|
assert "Competitive intel brief" in texts
|
|
assert "Acme has a new hiring signal." in texts
|
|
assert inspected["tables"][0][0] == ["Account", "Signal"]
|
|
assert inspected["tables"][0][1] == ["Acme", "hiring"]
|