324 lines
8.6 KiB
Python
324 lines
8.6 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from opensquilla.skills import dependency_summary
|
|
from opensquilla.skills.dependency_summary import build_dependency_summary
|
|
from opensquilla.skills.eligibility import EligibilityContext, EligibilityReport
|
|
from opensquilla.skills.loader import SkillLoader
|
|
|
|
|
|
def _write_skill(
|
|
base: Path,
|
|
name: str,
|
|
skill_md: str,
|
|
scripts: dict[str, str] | None = None,
|
|
) -> None:
|
|
skill_dir = base / name
|
|
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
(skill_dir / "SKILL.md").write_text(skill_md, encoding="utf-8")
|
|
if not scripts:
|
|
return
|
|
scripts_dir = skill_dir / "scripts"
|
|
scripts_dir.mkdir(parents=True, exist_ok=True)
|
|
for rel_path, content in scripts.items():
|
|
script_path = scripts_dir / rel_path
|
|
script_path.parent.mkdir(parents=True, exist_ok=True)
|
|
script_path.write_text(content, encoding="utf-8")
|
|
|
|
|
|
def test_summary_reports_declared_python_and_missing_env_any(tmp_path: Path) -> None:
|
|
_write_skill(
|
|
tmp_path,
|
|
"declared-skill",
|
|
"""---
|
|
name: declared-skill
|
|
description: Declares a uv package and envAny.
|
|
metadata:
|
|
opensquilla:
|
|
requires:
|
|
anyBins: [python]
|
|
envAny: [OPENROUTER_API_KEY, ARK_API_KEY]
|
|
install:
|
|
- id: pillow
|
|
kind: uv
|
|
label: Install Pillow
|
|
package: pillow
|
|
---
|
|
|
|
# body
|
|
""",
|
|
)
|
|
loader = SkillLoader(bundled_dir=tmp_path, snapshot_path=tmp_path / "snapshot.json")
|
|
spec = loader.get_by_name("declared-skill")
|
|
assert spec is not None
|
|
|
|
summary = build_dependency_summary(
|
|
spec,
|
|
loader=loader,
|
|
ctx=EligibilityContext(
|
|
os_name="linux",
|
|
has_bin_cache={"python": True},
|
|
env_cache={"OPENROUTER_API_KEY": None, "ARK_API_KEY": None},
|
|
),
|
|
)
|
|
|
|
assert summary["declared"]["python_packages"] == [
|
|
{
|
|
"install_id": "pillow",
|
|
"label": "Install Pillow",
|
|
"package": "pillow",
|
|
"module": "",
|
|
}
|
|
]
|
|
assert summary["declared"]["api_env"]["any"] == [
|
|
"OPENROUTER_API_KEY",
|
|
"ARK_API_KEY",
|
|
]
|
|
assert summary["missing"]["api_env"]["any"] == [
|
|
["OPENROUTER_API_KEY", "ARK_API_KEY"]
|
|
]
|
|
assert summary["missing"]["count"] == 1
|
|
assert summary["declaration_quality"] == "declared"
|
|
|
|
|
|
def test_summary_reports_missing_any_bins_group(tmp_path: Path) -> None:
|
|
_write_skill(
|
|
tmp_path,
|
|
"any-bin-skill",
|
|
"""---
|
|
name: any-bin-skill
|
|
description: Needs one binary from a missing set.
|
|
metadata:
|
|
opensquilla:
|
|
requires:
|
|
anyBins: [missing-a, missing-b]
|
|
---
|
|
|
|
# body
|
|
""",
|
|
)
|
|
loader = SkillLoader(bundled_dir=tmp_path, snapshot_path=tmp_path / "snapshot.json")
|
|
spec = loader.get_by_name("any-bin-skill")
|
|
assert spec is not None
|
|
|
|
summary = build_dependency_summary(
|
|
spec,
|
|
loader=loader,
|
|
ctx=EligibilityContext(
|
|
os_name="linux",
|
|
has_bin_cache={"missing-a": False, "missing-b": False},
|
|
),
|
|
)
|
|
|
|
assert summary["missing"]["binaries"]["any"] == [["missing-a", "missing-b"]]
|
|
assert summary["missing"]["count"] == 1
|
|
|
|
|
|
def test_summary_marks_undeclared_script_imports_as_not_enforced(tmp_path: Path) -> None:
|
|
_write_skill(
|
|
tmp_path,
|
|
"plot-skill",
|
|
"""---
|
|
name: plot-skill
|
|
description: Bare skill for inferred imports.
|
|
---
|
|
|
|
# body
|
|
""",
|
|
scripts={
|
|
"plot.py": "import json\nimport matplotlib.pyplot as plt\n",
|
|
},
|
|
)
|
|
loader = SkillLoader(bundled_dir=tmp_path, snapshot_path=tmp_path / "snapshot.json")
|
|
spec = loader.get_by_name("plot-skill")
|
|
assert spec is not None
|
|
|
|
summary = build_dependency_summary(spec, loader=loader, ctx=EligibilityContext(os_name="linux"))
|
|
|
|
assert summary["inferred"]["python_imports"] == [
|
|
{
|
|
"module": "matplotlib",
|
|
"source": "scripts/plot.py",
|
|
"not_enforced": True,
|
|
}
|
|
]
|
|
assert summary["declaration_quality"] == "undeclared_inferred"
|
|
|
|
|
|
def test_summary_does_not_flag_project_dependency_imports(tmp_path: Path) -> None:
|
|
_write_skill(
|
|
tmp_path,
|
|
"project-deps-skill",
|
|
"""---
|
|
name: project-deps-skill
|
|
description: Imports packages already provided by the project.
|
|
---
|
|
|
|
# body
|
|
""",
|
|
scripts={
|
|
"plot.py": "import httpx\nfrom bs4 import BeautifulSoup\n",
|
|
},
|
|
)
|
|
loader = SkillLoader(bundled_dir=tmp_path, snapshot_path=tmp_path / "snapshot.json")
|
|
spec = loader.get_by_name("project-deps-skill")
|
|
assert spec is not None
|
|
|
|
summary = build_dependency_summary(spec, loader=loader, ctx=EligibilityContext(os_name="linux"))
|
|
|
|
assert summary["inferred"]["python_imports"] == []
|
|
|
|
|
|
def test_summary_ignores_local_src_and_project_alias_imports(tmp_path: Path) -> None:
|
|
_write_skill(
|
|
tmp_path,
|
|
"local-import-skill",
|
|
"""---
|
|
name: local-import-skill
|
|
description: Imports a local module and a project dependency alias.
|
|
---
|
|
|
|
# body
|
|
""",
|
|
scripts={
|
|
"runner.py": "from src.video_merger import VideoMerger\nimport nio\n",
|
|
"src/video_merger.py": "class VideoMerger:\n pass\n",
|
|
},
|
|
)
|
|
loader = SkillLoader(bundled_dir=tmp_path, snapshot_path=tmp_path / "snapshot.json")
|
|
spec = loader.get_by_name("local-import-skill")
|
|
assert spec is not None
|
|
|
|
summary = build_dependency_summary(spec, loader=loader, ctx=EligibilityContext(os_name="linux"))
|
|
|
|
assert summary["inferred"]["python_imports"] == []
|
|
|
|
|
|
def test_meta_skill_rolls_up_sub_skill_dependency_issues(tmp_path: Path) -> None:
|
|
_write_skill(
|
|
tmp_path,
|
|
"child",
|
|
"""---
|
|
name: child
|
|
description: Needs a missing binary.
|
|
metadata:
|
|
opensquilla:
|
|
requires:
|
|
bins: [missing-tool]
|
|
---
|
|
|
|
# body
|
|
""",
|
|
)
|
|
_write_skill(
|
|
tmp_path,
|
|
"parent",
|
|
"""---
|
|
name: parent
|
|
description: Meta parent.
|
|
kind: meta
|
|
composition:
|
|
steps:
|
|
- id: child-step
|
|
skill: child
|
|
- id: route-step
|
|
routes:
|
|
- label: fallback
|
|
skill: missing-child
|
|
---
|
|
|
|
# body
|
|
""",
|
|
)
|
|
loader = SkillLoader(bundled_dir=tmp_path, snapshot_path=tmp_path / "snapshot.json")
|
|
spec = loader.get_by_name("parent")
|
|
assert spec is not None
|
|
|
|
summary = build_dependency_summary(
|
|
spec,
|
|
loader=loader,
|
|
ctx=EligibilityContext(os_name="linux", has_bin_cache={"missing-tool": False}),
|
|
)
|
|
|
|
assert summary["sub_skill_dependencies"]["missing_count"] == 1
|
|
assert summary["sub_skill_dependencies"]["missing_references"] == ["missing-child"]
|
|
assert summary["sub_skill_dependencies"]["skills"][0]["name"] == "child"
|
|
|
|
|
|
def test_summary_collects_scan_errors_without_crashing(tmp_path: Path) -> None:
|
|
_write_skill(
|
|
tmp_path,
|
|
"broken-script-skill",
|
|
"""---
|
|
name: broken-script-skill
|
|
description: Has a syntax error in a script.
|
|
---
|
|
|
|
API docs mention EXAMPLE_API_KEY.
|
|
""",
|
|
scripts={"broken.py": "def broken(:\n"},
|
|
)
|
|
loader = SkillLoader(bundled_dir=tmp_path, snapshot_path=tmp_path / "snapshot.json")
|
|
spec = loader.get_by_name("broken-script-skill")
|
|
assert spec is not None
|
|
|
|
summary = build_dependency_summary(spec, loader=loader, ctx=EligibilityContext(os_name="linux"))
|
|
|
|
assert summary["inferred"]["python_imports"] == []
|
|
assert summary["inferred"]["scan_errors"]
|
|
assert "scripts/broken.py" in summary["inferred"]["scan_errors"][0]
|
|
|
|
|
|
def test_summary_uses_precomputed_report_when_provided(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
_write_skill(
|
|
tmp_path,
|
|
"precomputed-report-skill",
|
|
"""---
|
|
name: precomputed-report-skill
|
|
description: Uses a precomputed eligibility report.
|
|
metadata:
|
|
opensquilla:
|
|
requires:
|
|
envAny: [OPENROUTER_API_KEY, ARK_API_KEY]
|
|
---
|
|
|
|
# body
|
|
""",
|
|
)
|
|
loader = SkillLoader(bundled_dir=tmp_path, snapshot_path=tmp_path / "snapshot.json")
|
|
spec = loader.get_by_name("precomputed-report-skill")
|
|
assert spec is not None
|
|
|
|
monkeypatch.setattr(
|
|
dependency_summary,
|
|
"diagnose_eligibility",
|
|
lambda _spec, _ctx: pytest.fail("diagnose_eligibility should not be called"),
|
|
)
|
|
report = EligibilityReport(
|
|
eligible=False,
|
|
missing_env_any=[["OPENROUTER_API_KEY", "ARK_API_KEY"]],
|
|
declared=True,
|
|
)
|
|
|
|
summary = build_dependency_summary(
|
|
spec,
|
|
loader=loader,
|
|
ctx=EligibilityContext(os_name="linux"),
|
|
report=report,
|
|
)
|
|
|
|
assert summary["declared"]["api_env"]["any"] == [
|
|
"OPENROUTER_API_KEY",
|
|
"ARK_API_KEY",
|
|
]
|
|
assert summary["missing"]["api_env"]["any"] == [
|
|
["OPENROUTER_API_KEY", "ARK_API_KEY"]
|
|
]
|
|
assert summary["missing"]["count"] == 1
|