Skip to content

Commit 71125fc

Browse files
test(integrations): guard multiline/control-char SKILL.md frontmatter escaping (#3392)
Add regression tests for SkillsIntegration mixin that verify: - Multiline (block-scalar) description round-trips byte-for-byte - C0/DEL control characters in description survive YAML escaping Tests properly isolate Path.home() for Hermes to prevent overwriting a developer's real global skill directory. Refs: #3392
1 parent f01cac6 commit 71125fc

1 file changed

Lines changed: 85 additions & 0 deletions

File tree

tests/integrations/test_integration_base_skills.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,91 @@ def test_skill_uses_template_descriptions(self, tmp_path):
141141
assert isinstance(fm["description"], str)
142142
assert len(fm["description"]) > 0, f"{f} has empty description"
143143

144+
def test_skill_frontmatter_preserves_multiline_description(
145+
self, tmp_path, monkeypatch
146+
):
147+
"""A multiline (block-scalar) description must round-trip exactly.
148+
149+
The hand-built SKILL.md frontmatter used to only escape backslash and
150+
quote, so a block-scalar description was emitted with raw newlines inside
151+
a double-quoted scalar and reparsed with those newlines collapsed to
152+
spaces. The description must survive byte-for-byte."""
153+
from pathlib import Path
154+
155+
i = get_integration(self.KEY)
156+
# Hermes writes to ~/.hermes/skills/ — isolate Path.home() to prevent
157+
# overwriting a developer's real global skill directory.
158+
if self.KEY == "hermes":
159+
home = tmp_path / "home"
160+
home.mkdir(exist_ok=True)
161+
monkeypatch.setattr(Path, "home", lambda: home)
162+
163+
template = tmp_path / "sample.md"
164+
template.write_text(
165+
"---\n"
166+
"description: |\n"
167+
" first line\n"
168+
" second line\n"
169+
"scripts:\n"
170+
" sh: scripts/bash/x.sh\n"
171+
"---\n"
172+
"Body\n",
173+
encoding="utf-8",
174+
)
175+
monkeypatch.setattr(i, "list_command_templates", lambda: [template])
176+
177+
m = IntegrationManifest(self.KEY, tmp_path)
178+
created = i.setup(tmp_path, m)
179+
skill_files = [f for f in created if f.name == "SKILL.md"]
180+
assert len(skill_files) == 1
181+
182+
content = skill_files[0].read_text(encoding="utf-8")
183+
fm = yaml.safe_load(content.split("---", 2)[1])
184+
assert "\n" in fm["description"]
185+
assert fm["description"] == "first line\nsecond line\n"
186+
187+
def test_skill_frontmatter_preserves_control_characters(
188+
self, tmp_path, monkeypatch
189+
):
190+
"""A description carrying a C0/DEL control char must round-trip exactly.
191+
192+
A control character can reach ``description`` via a YAML escape in the
193+
source template (``"a\\x08b"`` parses to a real U+0008). The old
194+
hand-built frontmatter only escaped backslash and quote, so the raw
195+
control char landed inside the emitted double-quoted scalar and made the
196+
SKILL.md unparseable / lossy. ``yaml_quote`` must escape it so the
197+
value survives byte-for-byte."""
198+
from pathlib import Path
199+
200+
i = get_integration(self.KEY)
201+
# Hermes writes to ~/.hermes/skills/ — isolate Path.home() to prevent
202+
# overwriting a developer's real global skill directory.
203+
if self.KEY == "hermes":
204+
home = tmp_path / "home"
205+
home.mkdir(exist_ok=True)
206+
monkeypatch.setattr(Path, "home", lambda: home)
207+
208+
template = tmp_path / "sample.md"
209+
template.write_text(
210+
"---\n"
211+
'description: "a\\x08b\\ttab"\n'
212+
"scripts:\n"
213+
" sh: scripts/bash/x.sh\n"
214+
"---\n"
215+
"Body\n",
216+
encoding="utf-8",
217+
)
218+
monkeypatch.setattr(i, "list_command_templates", lambda: [template])
219+
220+
m = IntegrationManifest(self.KEY, tmp_path)
221+
created = i.setup(tmp_path, m)
222+
skill_files = [f for f in created if f.name == "SKILL.md"]
223+
assert len(skill_files) == 1
224+
225+
content = skill_files[0].read_text(encoding="utf-8")
226+
fm = yaml.safe_load(content.split("---", 2)[1])
227+
assert fm["description"] == "a\x08b\ttab"
228+
144229
def test_templates_are_processed(self, tmp_path):
145230
"""Skill body must have placeholders replaced, not raw templates."""
146231
i = get_integration(self.KEY)

0 commit comments

Comments
 (0)