Skip to content

Commit 577d51c

Browse files
TMHSDigitalclaude
andcommitted
test: cover sync_from_registry, build_site and --fix; gate sibling-clone test
Adds fixture-based tests for sync_from_registry (marker replacement, backslash content, script-tag escaping, --check drift, LF output) and smoke tests that render a cursor-plugin and an mcp-server site through build_site.py with escaping checks. --fix helpers now insert a missing standards-version signal (frontmatter key or comment marker) instead of failing, matching the documented "missing or malformed" behavior, and are covered end to end through _apply_fixes_live. Drops the unused _FRONTMATTER_RE. The registry-vs-sibling-manifest test now runs only with DTD_SIBLING_REPOS=1, so a stale local clone no longer fails the suite. Closes #124 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com>
1 parent ed6ff13 commit 577d51c

6 files changed

Lines changed: 398 additions & 38 deletions

File tree

‎scripts/add_comment_marker.py‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
Usage: python add_comment_marker.py <file_path> <new_version>
44
55
Finds a line matching ``<!-- standards-version: X.Y.Z -->`` (anywhere in the
6-
file, though convention is the first line) and replaces the version. Exits 0
7-
on success, 1 if the marker is absent.
6+
file, though convention is the first line) and replaces the version. When
7+
no marker exists one is inserted as the first line. Exits 0 on success, 1 if
8+
the file cannot be read.
89
"""
910
from __future__ import annotations
1011

@@ -16,11 +17,14 @@
1617
_MARKER_RE = re.compile(r"<!--\s*standards-version\s*:\s*[^\s>]+\s*-->")
1718

1819

19-
def _update(text: str, new_version: str) -> str | None:
20-
"""Return updated text, or None if no marker is found."""
20+
def _update(text: str, new_version: str) -> str:
21+
"""Return *text* with the marker set to *new_version*, inserting the
22+
marker as the first line when it is absent."""
2123
new_marker = f"<!-- standards-version: {new_version} -->"
22-
updated, count = _MARKER_RE.subn(new_marker, text, count=1)
23-
return updated if count else None
24+
updated, count = _MARKER_RE.subn(lambda _m: new_marker, text, count=1)
25+
if count:
26+
return updated
27+
return new_marker + "\n" + text
2428

2529

2630
def main(argv: list[str]) -> int:
@@ -38,10 +42,6 @@ def main(argv: list[str]) -> int:
3842
return 1
3943

4044
updated = _update(text, new_version)
41-
if updated is None:
42-
print(f"error: standards-version marker not found in {path}", file=sys.stderr)
43-
return 1
44-
4545
path.write_text(updated, encoding="utf-8", newline="\n")
4646
print(f"updated {path}")
4747
return 0

‎scripts/add_frontmatter.py‎

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
Usage: python add_frontmatter.py <file_path> <new_version>
44
55
Finds the ``standards-version:`` key inside the leading ``---`` block and
6-
replaces its value. Exits 0 on success, 1 if the marker is absent or the
7-
file cannot be parsed.
6+
replaces its value. When the key is missing it is inserted as the first key
7+
of the existing frontmatter block, and when the file has no frontmatter at
8+
all a minimal block is prepended. Exits 0 on success, 1 if the file cannot
9+
be read.
810
"""
911
from __future__ import annotations
1012

@@ -13,28 +15,26 @@
1315
from pathlib import Path
1416

1517

16-
_FRONTMATTER_RE = re.compile(r"\Astandardsversion:", re.I) # loose sentinel
17-
18-
19-
def _update(text: str, new_version: str) -> str | None:
20-
"""Return updated text, or None if the standards-version key is absent."""
18+
def _update(text: str, new_version: str) -> str:
19+
"""Return *text* with ``standards-version`` set to *new_version*."""
2120
lines = text.splitlines(keepends=True)
22-
in_block = False
23-
for i, line in enumerate(lines):
24-
stripped = line.lstrip()
25-
if i == 0 and stripped.rstrip() == "---":
26-
in_block = True
27-
continue
28-
if in_block and stripped.rstrip() in ("---", "..."):
29-
break
30-
if in_block and re.match(r"^standards-version\s*:", stripped):
31-
lines[i] = re.sub(
32-
r"(standards-version\s*:\s*).*",
33-
rf"\g<1>{new_version}",
34-
line,
35-
)
36-
return "".join(lines)
37-
return None
21+
has_block = bool(lines) and lines[0].strip() == "---"
22+
if has_block:
23+
for i, line in enumerate(lines[1:], start=1):
24+
stripped = line.lstrip()
25+
if stripped.rstrip() in ("---", "..."):
26+
break
27+
if re.match(r"^standards-version\s*:", stripped):
28+
lines[i] = re.sub(
29+
r"(standards-version\s*:\s*).*",
30+
lambda m: m.group(1) + new_version,
31+
line,
32+
)
33+
return "".join(lines)
34+
# Block exists but lacks the key: insert it as the first key.
35+
lines.insert(1, f"standards-version: {new_version}\n")
36+
return "".join(lines)
37+
return f"---\nstandards-version: {new_version}\n---\n" + text
3838

3939

4040
def main(argv: list[str]) -> int:
@@ -52,10 +52,6 @@ def main(argv: list[str]) -> int:
5252
return 1
5353

5454
updated = _update(text, new_version)
55-
if updated is None:
56-
print(f"error: standards-version not found in frontmatter of {path}", file=sys.stderr)
57-
return 1
58-
5955
path.write_text(updated, encoding="utf-8", newline="\n")
6056
print(f"updated {path}")
6157
return 0

‎tests/test_build_site.py‎

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""Smoke tests for site-template/build_site.py: a minimal cursor-plugin
2+
repo and a minimal mcp-server repo must both render, and repo-supplied text
3+
must be HTML-escaped."""
4+
from __future__ import annotations
5+
6+
import json
7+
import subprocess
8+
import sys
9+
from pathlib import Path
10+
11+
import pytest
12+
13+
REPO_ROOT = Path(__file__).resolve().parent.parent
14+
BUILD_SITE = REPO_ROOT / "site-template" / "build_site.py"
15+
16+
pytest.importorskip("jinja2")
17+
18+
HOSTILE = '<script>alert(1)</script> & "quotes"'
19+
20+
21+
def _site(**extra) -> str:
22+
"""Minimal site.json matching what the scaffold emits."""
23+
site = {
24+
"accent": "#7c3aed",
25+
"accentLight": "#a78bfa",
26+
"heroGradientFrom": "#0d1117",
27+
"heroGradientTo": "#161b22",
28+
"links": {"github": "https://github.com/TMHSDigital/Sample"},
29+
"installSteps": ["Install it"],
30+
}
31+
site.update(extra)
32+
return json.dumps(site)
33+
34+
35+
def _write(path: Path, text: str) -> None:
36+
path.parent.mkdir(parents=True, exist_ok=True)
37+
path.write_text(text, encoding="utf-8")
38+
39+
40+
def _build(repo: Path, out: Path) -> str:
41+
result = subprocess.run(
42+
[sys.executable, str(BUILD_SITE), "--repo-root", str(repo), "--out", str(out)],
43+
capture_output=True,
44+
text=True,
45+
)
46+
assert result.returncode == 0, result.stderr
47+
return (out / "index.html").read_text(encoding="utf-8")
48+
49+
50+
def test_cursor_plugin_site_renders_and_escapes(tmp_path: Path):
51+
repo = tmp_path / "plugin"
52+
_write(repo / "site.json", _site(title="Sample Plugin", description=HOSTILE))
53+
_write(
54+
repo / ".cursor-plugin" / "plugin.json",
55+
json.dumps({
56+
"name": "sample-plugin",
57+
"displayName": "Sample Plugin",
58+
"description": HOSTILE,
59+
"version": "1.2.3",
60+
"repository": "https://github.com/TMHSDigital/Sample-Plugin",
61+
"license": "CC-BY-NC-ND-4.0",
62+
}),
63+
)
64+
_write(
65+
repo / "skills" / "first-skill" / "SKILL.md",
66+
"---\nname: first-skill\ndescription: Does the first thing\n---\n# First\n",
67+
)
68+
_write(repo / "rules" / "style.mdc", "---\ndescription: Style rule\n---\nBody\n")
69+
_write(
70+
repo / "mcp-tools.json",
71+
json.dumps([{"name": "sample_tool", "description": HOSTILE, "category": "General"}]),
72+
)
73+
_write(
74+
repo / "CHANGELOG.md",
75+
"# Changelog\n\n## [Unreleased]\n\n## [1.2.3] - 2026-01-01\n\n### Fixed\n\n- A fix\n",
76+
)
77+
78+
html = _build(repo, tmp_path / "out")
79+
assert "Sample Plugin" in html
80+
assert "Does the first thing" in html
81+
assert "sample_tool" in html
82+
assert "1.2.3" in html
83+
assert "<script>alert(1)</script>" not in html
84+
assert "&lt;script&gt;" in html
85+
86+
87+
def test_mcp_server_site_renders_from_package_json(tmp_path: Path):
88+
repo = tmp_path / "server"
89+
_write(repo / "site.json", _site(description="A sample server"))
90+
_write(
91+
repo / "package.json",
92+
json.dumps({
93+
"name": "@tmhs/sample-mcp",
94+
"version": "0.4.0",
95+
"description": "A sample server",
96+
"repository": {"url": "git+https://github.com/TMHSDigital/sample-mcp.git"},
97+
"license": "CC-BY-NC-ND-4.0",
98+
}),
99+
)
100+
_write(repo / "mcp-tools.json", json.dumps([{"name": "do_thing", "description": "x"}]))
101+
102+
html = _build(repo, tmp_path / "out")
103+
assert "Sample MCP" in html
104+
assert "do_thing" in html
105+
assert "0.4.0" in html

‎tests/test_fix_scripts.py‎

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""Tests for the --fix helpers (add_frontmatter.py, add_comment_marker.py)
2+
and the drift-check path that invokes them."""
3+
from __future__ import annotations
4+
5+
import importlib.util
6+
from pathlib import Path
7+
8+
from scripts.drift_check import cli
9+
from scripts.drift_check.semver import parse_version
10+
from scripts.drift_check.types import Finding, RepoConfig, RepoSnapshot
11+
12+
REPO_ROOT = Path(__file__).resolve().parent.parent
13+
SCRIPTS_DIR = REPO_ROOT / "scripts"
14+
15+
16+
def _load(name: str):
17+
spec = importlib.util.spec_from_file_location(name, SCRIPTS_DIR / f"{name}.py")
18+
module = importlib.util.module_from_spec(spec)
19+
spec.loader.exec_module(module)
20+
return module
21+
22+
23+
fm = _load("add_frontmatter")
24+
cm = _load("add_comment_marker")
25+
26+
27+
# ------------------------------- frontmatter --------------------------------
28+
29+
30+
def test_frontmatter_replaces_existing_value():
31+
text = "---\nname: x\nstandards-version: 1.0.0\n---\nbody\n"
32+
assert fm._update(text, "1.10.1") == "---\nname: x\nstandards-version: 1.10.1\n---\nbody\n"
33+
34+
35+
def test_frontmatter_inserts_key_into_existing_block():
36+
text = "---\nname: x\n---\nbody\n"
37+
assert fm._update(text, "1.10.1") == "---\nstandards-version: 1.10.1\nname: x\n---\nbody\n"
38+
39+
40+
def test_frontmatter_prepends_block_when_absent():
41+
text = "# Title\n"
42+
assert fm._update(text, "1.10.1") == "---\nstandards-version: 1.10.1\n---\n# Title\n"
43+
44+
45+
def test_frontmatter_ignores_key_outside_block():
46+
text = "---\nname: x\n---\nstandards-version: 0.0.1\n"
47+
out = fm._update(text, "2.0.0")
48+
assert out.startswith("---\nstandards-version: 2.0.0\nname: x\n---\n")
49+
assert out.endswith("standards-version: 0.0.1\n")
50+
51+
52+
def test_frontmatter_main_writes_file(tmp_path: Path):
53+
path = tmp_path / "SKILL.md"
54+
path.write_text("---\nname: x\n---\n", encoding="utf-8")
55+
assert fm.main(["add_frontmatter.py", str(path), "1.2.3"]) == 0
56+
assert "standards-version: 1.2.3" in path.read_text(encoding="utf-8")
57+
58+
59+
# ------------------------------- comment marker -----------------------------
60+
61+
62+
def test_marker_replaces_existing():
63+
text = "<!-- standards-version: 1.0.0 -->\n# Agents\n"
64+
assert cm._update(text, "1.10.1") == "<!-- standards-version: 1.10.1 -->\n# Agents\n"
65+
66+
67+
def test_marker_inserted_when_missing():
68+
assert cm._update("# Agents\n", "1.10.1") == "<!-- standards-version: 1.10.1 -->\n# Agents\n"
69+
70+
71+
def test_marker_main_on_missing_file_fails(tmp_path: Path):
72+
assert cm.main(["add_comment_marker.py", str(tmp_path / "nope.md"), "1.0.0"]) == 1
73+
74+
75+
# ------------------------------- --fix wiring -------------------------------
76+
77+
78+
def test_apply_fixes_live_inserts_missing_signal(tmp_path: Path):
79+
repo = tmp_path / "repo"
80+
(repo / "skills" / "a").mkdir(parents=True)
81+
skill = repo / "skills" / "a" / "SKILL.md"
82+
skill.write_text("---\nname: a\n---\nbody\n", encoding="utf-8")
83+
agents = repo / "AGENTS.md"
84+
agents.write_text("# Agents\n", encoding="utf-8")
85+
86+
meta = parse_version("1.10.1")
87+
snap = RepoSnapshot(
88+
slug="repo",
89+
repo_type="cursor-plugin",
90+
files={},
91+
meta_version=meta,
92+
meta_commit="sha",
93+
config=RepoConfig(slug="repo", repo_type="cursor-plugin"),
94+
)
95+
findings = [
96+
Finding(repo="repo", file=Path("skills/a/SKILL.md"), check="version-signal",
97+
severity="error", message="missing standards-version"),
98+
Finding(repo="repo", file=Path("AGENTS.md"), check="version-signal",
99+
severity="error", message="missing standards-version"),
100+
]
101+
touched = cli._apply_fixes_live([snap], [repo], findings, SCRIPTS_DIR, meta)
102+
assert touched == 2
103+
assert "standards-version: 1.10.1" in skill.read_text(encoding="utf-8")
104+
assert agents.read_text(encoding="utf-8").startswith("<!-- standards-version: 1.10.1 -->")

‎tests/test_registry_manifest_counts.py‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"""
1212

1313
import json
14+
import os
1415
from pathlib import Path
1516

1617
import pytest
@@ -30,6 +31,13 @@ def _sibling_checkout(repo_field: str) -> Path | None:
3031
return candidate if candidate.is_dir() else None
3132

3233

34+
@pytest.mark.skipif(
35+
os.environ.get("DTD_SIBLING_REPOS") != "1",
36+
reason=(
37+
"set DTD_SIBLING_REPOS=1 to compare against sibling tool checkouts; "
38+
"off by default so a stale local clone cannot fail the suite"
39+
),
40+
)
3341
def test_registry_mcptools_matches_local_manifests():
3442
mismatches = []
3543
checked = []
@@ -57,7 +65,10 @@ def test_registry_mcptools_matches_local_manifests():
5765
"validation runs CI-side where repos can be cloned"
5866
)
5967

60-
assert not mismatches, "registry mcpTools != manifest length: " + ", ".join(
68+
assert not mismatches, (
69+
"registry mcpTools != manifest length (run `git pull` in each sibling "
70+
"checkout first; a stale clone reports false drift): "
71+
) + ", ".join(
6172
f"{slug}: registry={declared} manifest={actual}"
6273
for slug, declared, actual in mismatches
6374
)

0 commit comments

Comments
 (0)