main
py 215 lines 6.31 KB
Raw
1 import importlib.util
2 import json
3 import sys
4 import types
5 import zipfile
6 from pathlib import Path
7
8 import pytest
9
10
11 PROJECT_ROOT = Path(__file__).resolve().parents[1]
12 if str(PROJECT_ROOT) not in sys.path:
13 sys.path.insert(0, str(PROJECT_ROOT))
14
15
16 def _load_skills_import_module(tmp_path):
17 module_name = "test_skills_import_module"
18 stub_names = ("helpers", "helpers.files", "helpers.skills")
19 missing = object()
20 originals = {name: sys.modules.get(name, missing) for name in stub_names}
21
22 helpers_pkg = types.ModuleType("helpers")
23 helpers_pkg.__path__ = []
24
25 files = types.ModuleType("helpers.files")
26 files.get_abs_path = lambda *parts: str(tmp_path.joinpath(*parts))
27
28 skills = types.ModuleType("helpers.skills")
29 skills.discover_skill_md_files = lambda root: sorted(Path(root).rglob("SKILL.md"))
30
31 helpers_pkg.files = files
32 helpers_pkg.skills = skills
33 sys.modules["helpers"] = helpers_pkg
34 sys.modules["helpers.files"] = files
35 sys.modules["helpers.skills"] = skills
36
37 try:
38 spec = importlib.util.spec_from_file_location(
39 module_name,
40 PROJECT_ROOT / "helpers" / "skills_import.py",
41 )
42 module = importlib.util.module_from_spec(spec)
43 assert spec and spec.loader
44 sys.modules[module_name] = module
45 spec.loader.exec_module(module)
46 return module
47 finally:
48 for name, original in originals.items():
49 if original is missing:
50 sys.modules.pop(name, None)
51 else:
52 sys.modules[name] = original
53
54
55 def test_extract_skills_zip_rejects_path_traversal(monkeypatch, tmp_path):
56 skills_import = _load_skills_import_module(tmp_path)
57
58 archive_path = tmp_path / "bad.zip"
59 with zipfile.ZipFile(archive_path, "w") as archive:
60 archive.writestr("../escape.txt", "nope")
61
62 with pytest.raises(ValueError, match="Unsafe zip entry path"):
63 skills_import.extract_skills_zip(archive_path)
64
65 assert not (tmp_path / "escape.txt").exists()
66
67
68 def test_extract_skills_zip_returns_single_top_level_root(monkeypatch, tmp_path):
69 skills_import = _load_skills_import_module(tmp_path)
70
71 archive_path = tmp_path / "skills.zip"
72 with zipfile.ZipFile(archive_path, "w") as archive:
73 archive.writestr("pack/example/SKILL.md", "---\nname: Example\n---\nBody\n")
74
75 source_root, cleanup_root = skills_import.extract_skills_zip(archive_path)
76
77 assert source_root.name == "pack"
78 assert cleanup_root.is_dir()
79 assert (source_root / "example" / "SKILL.md").is_file()
80
81
82 def test_settings_skills_scan_section_and_prompt_assets_are_present():
83 settings_store = (
84 PROJECT_ROOT
85 / "webui"
86 / "components"
87 / "settings"
88 / "settings-store.js"
89 ).read_text(encoding="utf-8")
90 skills_settings = (
91 PROJECT_ROOT
92 / "webui"
93 / "components"
94 / "settings"
95 / "skills"
96 / "skills-settings.html"
97 ).read_text(encoding="utf-8")
98 import_template = (
99 PROJECT_ROOT
100 / "webui"
101 / "components"
102 / "settings"
103 / "skills"
104 / "import.html"
105 ).read_text(encoding="utf-8")
106 scan_prompt = (
107 PROJECT_ROOT
108 / "webui"
109 / "components"
110 / "settings"
111 / "skills"
112 / "skill-scan-prompt.md"
113 ).read_text(encoding="utf-8")
114 scan_checks = json.loads(
115 (
116 PROJECT_ROOT
117 / "webui"
118 / "components"
119 / "settings"
120 / "skills"
121 / "skill-scan-checks.json"
122 ).read_text(encoding="utf-8")
123 )
124 scan_checks_text = json.dumps(scan_checks)
125
126 assert "section-skills-scan" in settings_store
127 assert "settings/skills/scan.html" in skills_settings
128 assert "settings/skills/import.html" in skills_settings
129 assert settings_store.index("section-skills-import") < settings_store.index(
130 "section-skills-scan"
131 )
132 assert skills_settings.index("section-skills-import") < skills_settings.index(
133 "section-skills-scan"
134 )
135 assert "scanSelectedFile()" in import_template
136 assert "snyk-agent-scan@latest --json --no-bootstrap" in scan_prompt
137 assert "E004" in scan_checks_text
138 assert "W007" in scan_checks_text
139 assert "W008" in scan_checks_text
140 assert "W011" in scan_checks_text
141 assert "W012" in scan_checks_text
142
143
144 def test_list_and_import_skills_filters_are_project_only():
145 list_template = (
146 PROJECT_ROOT
147 / "webui"
148 / "components"
149 / "settings"
150 / "skills"
151 / "list.html"
152 ).read_text(encoding="utf-8")
153 list_store = (
154 PROJECT_ROOT
155 / "webui"
156 / "components"
157 / "settings"
158 / "skills"
159 / "skills-list-store.js"
160 ).read_text(encoding="utf-8")
161 import_template = (
162 PROJECT_ROOT
163 / "webui"
164 / "components"
165 / "settings"
166 / "skills"
167 / "import.html"
168 ).read_text(encoding="utf-8")
169 import_store = (
170 PROJECT_ROOT
171 / "webui"
172 / "components"
173 / "settings"
174 / "skills"
175 / "skills-import-store.js"
176 ).read_text(encoding="utf-8")
177
178 assert "Agent profile" not in list_template
179 assert "agentProfile" not in list_store
180 assert "agent_profile" not in list_store
181 assert "Project" in list_template
182 assert "project_name" in list_store
183 assert "Limit to agent profile" not in import_template
184 assert "agentProfile" not in import_store
185 assert "agent_profile" not in import_store
186 assert "Limit to project" in import_template
187 assert "project_name" in import_store
188
189
190 def test_list_skills_has_mcp_style_search():
191 list_template = (
192 PROJECT_ROOT
193 / "webui"
194 / "components"
195 / "settings"
196 / "skills"
197 / "list.html"
198 ).read_text(encoding="utf-8")
199 list_store = (
200 PROJECT_ROOT
201 / "webui"
202 / "components"
203 / "settings"
204 / "skills"
205 / "skills-list-store.js"
206 ).read_text(encoding="utf-8")
207
208 assert 'type="search"' in list_template
209 assert "placeholder=\"Search skills\"" in list_template
210 assert "skills-search-clear" in list_template
211 assert "filteredSkills" in list_template
212 assert "No skills match this search." in list_template
213 assert "skillSearch" in list_store
214 assert "matchesSearchQuery" in list_store
215 assert "clearSkillSearch()" in list_store