📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The catalog snapshot must not depend on the checkout's line endings.
|
||||
|
||||
Regression test for bd19ab9 (#462), where catalog-summary.json was regenerated
|
||||
on a CRLF checkout. Every recorded sha256 was the CRLF hash of the source file,
|
||||
so `verify:data` failed on every LF platform, including CI.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO = next(
|
||||
parent for parent in Path(__file__).resolve().parents
|
||||
if (parent / "scripts" / "generate-catalog-summary.py").is_file()
|
||||
)
|
||||
DATA = REPO / "src/ui-ux-pro-max/data"
|
||||
SNAPSHOT_FILES = (
|
||||
"google-fonts.csv",
|
||||
"google-font-licenses.json",
|
||||
"icons.csv",
|
||||
"phosphor-icons-upstream.json",
|
||||
)
|
||||
|
||||
|
||||
def _load_generator():
|
||||
path = REPO / "scripts" / "generate-catalog-summary.py"
|
||||
spec = importlib.util.spec_from_file_location("generate_catalog_summary", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class CatalogSummaryLineEndingsTest(unittest.TestCase):
|
||||
def test_digest_is_identical_for_lf_and_crlf(self):
|
||||
digest = _load_generator().digest
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
lf = Path(tmp) / "lf.csv"
|
||||
crlf = Path(tmp) / "crlf.csv"
|
||||
lf.write_bytes(b"id,name\n1,alpha\n2,beta\n")
|
||||
crlf.write_bytes(b"id,name\r\n1,alpha\r\n2,beta\r\n")
|
||||
self.assertEqual(
|
||||
digest(lf), digest(crlf),
|
||||
"snapshot hashes must not change with the checkout's line endings",
|
||||
)
|
||||
|
||||
def test_committed_snapshot_matches_normalized_sources(self):
|
||||
summary = json.loads((DATA / "catalog-summary.json").read_text(encoding="utf-8"))
|
||||
for name in SNAPSHOT_FILES:
|
||||
expected = hashlib.sha256(
|
||||
(DATA / name).read_bytes().replace(b"\r\n", b"\n")
|
||||
).hexdigest()
|
||||
self.assertEqual(
|
||||
summary["snapshots"][name]["sha256"], expected,
|
||||
f"{name}: committed snapshot hash does not match the LF-normalized source",
|
||||
)
|
||||
|
||||
def test_crlf_checkout_produces_the_committed_hashes(self):
|
||||
"""Simulate a Windows checkout: the recorded hashes must still validate."""
|
||||
digest = _load_generator().digest
|
||||
summary = json.loads((DATA / "catalog-summary.json").read_text(encoding="utf-8"))
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
for name in SNAPSHOT_FILES:
|
||||
crlf_copy = Path(tmp) / name
|
||||
raw = (DATA / name).read_bytes().replace(b"\r\n", b"\n")
|
||||
crlf_copy.write_bytes(raw.replace(b"\n", b"\r\n"))
|
||||
self.assertEqual(
|
||||
digest(crlf_copy), summary["snapshots"][name]["sha256"],
|
||||
f"{name}: a CRLF checkout would record a different hash",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Every script invocation in the shipped skill markdown resolves from the skill directory.
|
||||
|
||||
Regression test for #474. The sub-skills ship in two copies (.claude/skills/<skill>/
|
||||
for the plugin, cli/assets/skills/<skill>/ for CLI installs) and land in layouts where
|
||||
neither the project root nor ~/.claude/skills/ is a valid anchor: the plugin cache, a
|
||||
project's .claude/skills/, ~/.claude/skills/ (--global), or a manual copy. The one anchor
|
||||
that exists in all of them is the skill's own directory, so documented commands use
|
||||
`scripts/<file>` for the skill's own scripts and `../<skill>/scripts/<file>` for a
|
||||
sibling sub-skill (the sub-skills are always installed side by side).
|
||||
|
||||
This test extracts every `python|python3|node|bash <path>` invocation from every
|
||||
markdown file under both trees and asserts that the path is skill-relative and names a
|
||||
file that ships. The core skill's `${CLAUDE_PLUGIN_ROOT}/.claude/skills/...` form is
|
||||
resolved against the repository root, which is what that variable denotes under a
|
||||
plugin install - and accepted only in that file, because the sub-skills also ship
|
||||
through the CLI, where the variable does not exist. The grep-based path contract in check-asset-sync.yml is the negative
|
||||
side (no home-, project- or variable-rooted paths anywhere, code included); this is
|
||||
the positive side (every documented invocation points at a real file).
|
||||
"""
|
||||
|
||||
import re
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO = next(
|
||||
parent for parent in Path(__file__).resolve().parents
|
||||
if (parent / "scripts" / "generate-catalog-summary.py").is_file()
|
||||
)
|
||||
SKILL_TREES = ("cli/assets/skills", ".claude/skills")
|
||||
# The only file that may use the plugin-root form: hand-authored for the plugin install
|
||||
# and not shipped by the CLI (sync-assets.mjs mirrors data/ and scripts/, never SKILL.md).
|
||||
# (Built from segments: the path contract in check-asset-sync.yml scans this file too.)
|
||||
PLUGIN_ONLY_FILE = Path(".claude") / "skills" / "ui-ux-pro-max" / "SKILL.md"
|
||||
INVOCATION = re.compile(r'(?<![\w/.-])(?:python3?|node|bash)\s+"?([^\s"`\']+\.(?:py|cjs|js|mjs|sh))')
|
||||
PLUGIN_ROOT = "${CLAUDE_PLUGIN_ROOT}/"
|
||||
|
||||
|
||||
def shipped_invocations():
|
||||
for tree in SKILL_TREES:
|
||||
for skill_dir in sorted((REPO / tree).iterdir()):
|
||||
if not skill_dir.is_dir():
|
||||
continue
|
||||
for md in sorted(skill_dir.rglob("*.md")):
|
||||
for lineno, line in enumerate(md.read_text(encoding="utf-8").splitlines(), 1):
|
||||
for match in INVOCATION.finditer(line):
|
||||
yield skill_dir, md, lineno, match.group(1)
|
||||
|
||||
|
||||
def resolve(skill_dir, md, path):
|
||||
"""Return (target, None) for a skill-relative path, or (None, reason)."""
|
||||
if path.startswith(PLUGIN_ROOT):
|
||||
if md.relative_to(REPO) != PLUGIN_ONLY_FILE:
|
||||
return None, "the ${CLAUDE_PLUGIN_ROOT} form is only valid in the plugin-only core SKILL.md"
|
||||
return REPO / path[len(PLUGIN_ROOT):], None
|
||||
if path.startswith("scripts/"):
|
||||
return skill_dir / path, None
|
||||
if path.startswith("../"):
|
||||
parts = path.split("/")
|
||||
if len(parts) > 3 and parts[2] == "scripts" and (skill_dir.parent / parts[1]).is_dir():
|
||||
return skill_dir.parent / parts[1] / "/".join(parts[2:]), None
|
||||
return None, "a sibling invocation must be ../<skill>/scripts/<file> and the sibling must ship"
|
||||
return None, "not skill-relative (expected scripts/<file> or ../<skill>/scripts/<file>)"
|
||||
|
||||
|
||||
class SkillScriptPathsTest(unittest.TestCase):
|
||||
def test_every_shipped_markdown_invocation_resolves_from_the_skill_directory(self):
|
||||
problems, seen = [], 0
|
||||
for skill_dir, md, lineno, path in shipped_invocations():
|
||||
seen += 1
|
||||
target, reason = resolve(skill_dir, md, path)
|
||||
if reason is None and not target.is_file():
|
||||
reason = f"no such file: {target}"
|
||||
if reason:
|
||||
problems.append(f"{md.relative_to(REPO)}:{lineno}: {path} -- {reason}")
|
||||
# Guard against a silently broken extractor: the two trees carry well over
|
||||
# a hundred documented invocations between them.
|
||||
self.assertGreater(seen, 100, f"extractor found only {seen} invocations")
|
||||
self.assertEqual(problems, [], "\n" + "\n".join(problems))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user