mirror of
https://github.com/github/spec-kit.git
synced 2026-08-15 14:02:04 +02:00
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
This commit is contained in:
@@ -141,6 +141,91 @@ class SkillsIntegrationTests:
|
||||
assert isinstance(fm["description"], str)
|
||||
assert len(fm["description"]) > 0, f"{f} has empty description"
|
||||
|
||||
def test_skill_frontmatter_preserves_multiline_description(
|
||||
self, tmp_path, monkeypatch
|
||||
):
|
||||
"""A multiline (block-scalar) description must round-trip exactly.
|
||||
|
||||
The hand-built SKILL.md frontmatter used to only escape backslash and
|
||||
quote, so a block-scalar description was emitted with raw newlines inside
|
||||
a double-quoted scalar and reparsed with those newlines collapsed to
|
||||
spaces. The description must survive byte-for-byte."""
|
||||
from pathlib import Path
|
||||
|
||||
i = get_integration(self.KEY)
|
||||
# Hermes writes to ~/.hermes/skills/ — isolate Path.home() to prevent
|
||||
# overwriting a developer's real global skill directory.
|
||||
if self.KEY == "hermes":
|
||||
home = tmp_path / "home"
|
||||
home.mkdir(exist_ok=True)
|
||||
monkeypatch.setattr(Path, "home", lambda: home)
|
||||
|
||||
template = tmp_path / "sample.md"
|
||||
template.write_text(
|
||||
"---\n"
|
||||
"description: |\n"
|
||||
" first line\n"
|
||||
" second line\n"
|
||||
"scripts:\n"
|
||||
" sh: scripts/bash/x.sh\n"
|
||||
"---\n"
|
||||
"Body\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(i, "list_command_templates", lambda: [template])
|
||||
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
created = i.setup(tmp_path, m)
|
||||
skill_files = [f for f in created if f.name == "SKILL.md"]
|
||||
assert len(skill_files) == 1
|
||||
|
||||
content = skill_files[0].read_text(encoding="utf-8")
|
||||
fm = yaml.safe_load(content.split("---", 2)[1])
|
||||
assert "\n" in fm["description"]
|
||||
assert fm["description"] == "first line\nsecond line\n"
|
||||
|
||||
def test_skill_frontmatter_preserves_control_characters(
|
||||
self, tmp_path, monkeypatch
|
||||
):
|
||||
"""A description carrying a C0/DEL control char must round-trip exactly.
|
||||
|
||||
A control character can reach ``description`` via a YAML escape in the
|
||||
source template (``"a\\x08b"`` parses to a real U+0008). The old
|
||||
hand-built frontmatter only escaped backslash and quote, so the raw
|
||||
control char landed inside the emitted double-quoted scalar and made the
|
||||
SKILL.md unparseable / lossy. ``yaml_quote`` must escape it so the
|
||||
value survives byte-for-byte."""
|
||||
from pathlib import Path
|
||||
|
||||
i = get_integration(self.KEY)
|
||||
# Hermes writes to ~/.hermes/skills/ — isolate Path.home() to prevent
|
||||
# overwriting a developer's real global skill directory.
|
||||
if self.KEY == "hermes":
|
||||
home = tmp_path / "home"
|
||||
home.mkdir(exist_ok=True)
|
||||
monkeypatch.setattr(Path, "home", lambda: home)
|
||||
|
||||
template = tmp_path / "sample.md"
|
||||
template.write_text(
|
||||
"---\n"
|
||||
'description: "a\\x08b\\ttab"\n'
|
||||
"scripts:\n"
|
||||
" sh: scripts/bash/x.sh\n"
|
||||
"---\n"
|
||||
"Body\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(i, "list_command_templates", lambda: [template])
|
||||
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
created = i.setup(tmp_path, m)
|
||||
skill_files = [f for f in created if f.name == "SKILL.md"]
|
||||
assert len(skill_files) == 1
|
||||
|
||||
content = skill_files[0].read_text(encoding="utf-8")
|
||||
fm = yaml.safe_load(content.split("---", 2)[1])
|
||||
assert fm["description"] == "a\x08b\ttab"
|
||||
|
||||
def test_templates_are_processed(self, tmp_path):
|
||||
"""Skill body must have placeholders replaced, not raw templates."""
|
||||
i = get_integration(self.KEY)
|
||||
|
||||
Reference in New Issue
Block a user