| 1 | import re |
| 2 | import unittest |
| 3 | from pathlib import Path |
| 4 | |
| 5 | from lib.skill_meta import read_skill_version |
| 6 | |
| 7 | ROOT = Path(__file__).resolve().parents[1] |
| 8 | SKILL_ROOT = ROOT / "skills" / "last30days" |
| 9 | |
| 10 | |
| 11 | def _skill_version() -> str: |
| 12 | version = read_skill_version(SKILL_ROOT / "SKILL.md") |
| 13 | if not version: |
| 14 | raise AssertionError("SKILL.md version frontmatter not found") |
| 15 | return version |
| 16 | |
| 17 | |
| 18 | class TestVersionConsistency(unittest.TestCase): |
| 19 | def test_skill_md_uses_double_quoted_version(self) -> None: |
| 20 | # The shared VERSION_RE in skill_meta.py accepts double-quoted, |
| 21 | # single-quoted, and unquoted YAML version scalars. This repo's |
| 22 | # SKILL.md must use the double-quoted form so the badge string stays |
| 23 | # deterministic and contributors don't accidentally introduce a |
| 24 | # quoting style that's harder for downstream tooling to parse. |
| 25 | text = (SKILL_ROOT / "SKILL.md").read_text(encoding="utf-8") |
| 26 | self.assertRegex( |
| 27 | text, |
| 28 | re.compile(r'^version:\s*"[^"]+"\s*$', re.MULTILINE), |
| 29 | msg="SKILL.md frontmatter version must use double-quoted form", |
| 30 | ) |
| 31 | |
| 32 | def test_root_skill_header_matches_frontmatter_version(self) -> None: |
| 33 | text = (SKILL_ROOT / "SKILL.md").read_text(encoding="utf-8") |
| 34 | version = _skill_version() |
| 35 | self.assertIn(f"# last30days v{version}:", text) |
| 36 | |
| 37 | def test_memory_save_dir_uses_single_env_variable(self) -> None: |
| 38 | skill_text = (SKILL_ROOT / "SKILL.md").read_text(encoding="utf-8") |
| 39 | compare_text = (SKILL_ROOT / "scripts" / "compare.sh").read_text(encoding="utf-8") |
| 40 | default_assignment = 'LAST30DAYS_MEMORY_DIR="${LAST30DAYS_MEMORY_DIR:-$HOME/Documents/Last30Days}"' |
| 41 | |
| 42 | self.assertIn(default_assignment, skill_text) |
| 43 | self.assertIn(default_assignment, compare_text) |
| 44 | self.assertNotIn("--save-dir=~/Documents/Last30Days", skill_text) |
| 45 | self.assertIn('--save-dir="${LAST30DAYS_MEMORY_DIR}"', skill_text) |
| 46 | |
| 47 | def test_compare_script_does_not_skip_permissions(self) -> None: |
| 48 | compare_text = (SKILL_ROOT / "scripts" / "compare.sh").read_text(encoding="utf-8") |
| 49 | |
| 50 | self.assertNotIn("--dangerously-skip-permissions", compare_text) |
| 51 | |
| 52 | def test_no_stray_hardcoded_memory_dir_paths(self) -> None: |
| 53 | allowed_suffixes = {".md", ".py", ".sh", ".txt", ".yml", ".yaml", ".json"} |
| 54 | skip_dirs = {".git", "assets", "fixtures", "docs"} |
| 55 | offenders = [] |
| 56 | |
| 57 | for path in ROOT.rglob("*"): |
| 58 | if not path.is_file() or path.suffix not in allowed_suffixes: |
| 59 | continue |
| 60 | if skip_dirs.intersection(path.relative_to(ROOT).parts): |
| 61 | continue |
| 62 | if path.relative_to(ROOT) == Path("tests/test_version_consistency.py"): |
| 63 | continue |
| 64 | |
| 65 | try: |
| 66 | lines = path.read_text(encoding="utf-8").splitlines() |
| 67 | except UnicodeDecodeError: |
| 68 | continue |
| 69 | |
| 70 | for line_number, line in enumerate(lines, start=1): |
| 71 | if "~/Documents/Last30Days" not in line and "$HOME/Documents/Last30Days" not in line: |
| 72 | continue |
| 73 | allowed_default = ( |
| 74 | "LAST30DAYS_MEMORY_DIR" in line |
| 75 | and ( |
| 76 | "defaults to" in line |
| 77 | or ( |
| 78 | path.parent == ROOT |
| 79 | and path.name.startswith("README.") |
| 80 | and path.name.endswith(".md") |
| 81 | ) |
| 82 | or "${LAST30DAYS_MEMORY_DIR:-$HOME/Documents/Last30Days}" in line |
| 83 | ) |
| 84 | ) |
| 85 | if not allowed_default: |
| 86 | offenders.append(f"{path.relative_to(ROOT)}:{line_number}: {line.strip()}") |
| 87 | |
| 88 | self.assertEqual([], offenders) |
| 89 | |
| 90 | if __name__ == "__main__": |
| 91 | unittest.main() |
| 92 |