run.sh 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. #!/usr/bin/env bash
  2. # Offline self-test for the threejs-ops skill — structure, frontmatter, script contract.
  3. #
  4. # Usage: tests/run.sh
  5. # Input: none (self-contained; no network, no browser)
  6. # Output: TAP-ish progress on stderr; final PASS/FAIL line.
  7. # Exit: 0 all pass (or skipped on unsupported platform), 1 any failure.
  8. #
  9. # Examples:
  10. # tests/run.sh
  11. # bash skills/threejs-ops/tests/run.sh
  12. set -uo pipefail
  13. here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
  14. fail=0
  15. pass=0
  16. note() { printf ' %s %s\n' "$1" "$2" >&2; }
  17. ok() { pass=$((pass+1)); note "ok " "$1"; }
  18. bad() { fail=$((fail+1)); note "FAIL" "$1"; }
  19. # Resolve a *working* python (python3, else python). The bare `command -v` is not
  20. # enough on Windows, where `python3` is a Microsoft Store stub that exits nonzero.
  21. PY=""
  22. for cand in python3 python; do
  23. if command -v "$cand" >/dev/null 2>&1 && "$cand" --version >/dev/null 2>&1; then
  24. PY="$cand"; break
  25. fi
  26. done
  27. if [ -z "$PY" ]; then
  28. echo "SKIP: no working python interpreter on this platform" >&2
  29. exit 0
  30. fi
  31. # 1. Required directories exist
  32. for d in scripts references assets tests; do
  33. [ -d "$here/$d" ] && ok "dir $d/ exists" || bad "missing dir $d/"
  34. done
  35. # 2. SKILL.md frontmatter house rules (SKILL-SUBAGENT-REFERENCE)
  36. skill="$here/SKILL.md"
  37. if [ -f "$skill" ]; then
  38. ok "SKILL.md present"
  39. grep -q '^name: threejs-ops$' "$skill" && ok "name matches directory" || bad "name != threejs-ops"
  40. grep -q '^license: MIT$' "$skill" && ok "license: MIT" || bad "missing license: MIT"
  41. grep -q '^ author: claude-mods$' "$skill" && ok "metadata.author" || bad "missing metadata.author"
  42. else
  43. bad "SKILL.md missing"
  44. fi
  45. # 3. Every reference on disk is cited from SKILL.md (no dead weight)
  46. for ref in "$here"/references/*.md; do
  47. base="references/$(basename "$ref")"
  48. grep -qF "$base" "$skill" && ok "cited: $base" || bad "uncited reference: $base"
  49. done
  50. # 4. Every SKILL.md-cited bundled resource exists on disk
  51. for res in assets/importmap-starter.html assets/three-facts.json scripts/check-three-facts.py; do
  52. [ -f "$here/$res" ] && ok "resource present: $res" || bad "missing resource: $res"
  53. done
  54. # 5. check-three-facts.py — staleness verifier contract (§7, §10), offline-safe
  55. facts="$here/scripts/check-three-facts.py"
  56. if [ -f "$facts" ]; then
  57. "$PY" -m py_compile "$facts" && ok "facts: py_compile clean" || bad "facts: py_compile failed"
  58. # Captured, not `head | grep -q`: under `set -o pipefail` grep -q's early exit
  59. # SIGPIPEs the producer (141) and flakes the assert even on a match.
  60. facts_head="$(head -35 "$facts")"
  61. grep -Eq '^# +Examples:' <<<"$facts_head" && ok "facts: has Examples block" || bad "facts: no Examples block"
  62. "$PY" "$facts" --help >/dev/null 2>&1 && ok "facts: --help exits 0" || bad "facts: --help nonzero"
  63. # Offline mode must pass on the skill's own content (internal consistency).
  64. "$PY" "$facts" --offline >/dev/null 2>&1 && ok "facts: --offline consistent (exit 0)" || bad "facts: --offline found inconsistency"
  65. # Bad flag → USAGE (exit 2); stays offline.
  66. "$PY" "$facts" --bogus >/dev/null 2>&1
  67. [ "$?" -eq 2 ] && ok "facts: bad flag → exit 2 (USAGE)" || bad "facts: bad flag did not exit 2"
  68. # --offline and --live are mutually exclusive → USAGE.
  69. "$PY" "$facts" --offline --live >/dev/null 2>&1
  70. [ "$?" -eq 2 ] && ok "facts: --offline --live → exit 2 (USAGE)" || bad "facts: conflicting modes did not exit 2"
  71. # stdout is data-only: --offline --json must emit parseable JSON with no stderr leakage.
  72. "$PY" "$facts" --offline --json -q 2>/dev/null | "$PY" -c 'import json,sys; d=json.load(sys.stdin); assert d["meta"]["schema"].startswith("claude-mods.threejs-ops")' \
  73. && ok "facts: --json envelope parses (stdout clean)" || bad "facts: --json envelope broken"
  74. # cited from SKILL.md
  75. grep -qF "scripts/check-three-facts.py" "$skill" && ok "facts: cited from SKILL.md" || bad "facts: uncited"
  76. else
  77. bad "check-three-facts.py missing"
  78. fi
  79. # 6. three-facts.json — parses, carries schema + gates the verifier depends on
  80. fj="$here/assets/three-facts.json"
  81. "$PY" -c "
  82. import json, sys
  83. d = json.load(open(sys.argv[1], encoding='utf-8'))
  84. assert d['schema'] == 'claude-mods.threejs-ops.facts/v1'
  85. assert d['version_gates']['examples_js_removed'] == 'r148'
  86. assert d['version_gates']['umd_builds_removed'] == 'r160'
  87. assert d['packages'], 'no packages committed'
  88. " "$fj" && ok "three-facts.json schema + gates" || bad "three-facts.json invalid"
  89. # 7. importmap-starter.html — import map parses, same pinned version both entries
  90. "$PY" -c "
  91. import json, re, sys
  92. html = open(sys.argv[1], encoding='utf-8').read()
  93. m = re.search(r'<script type=\"importmap\">\s*(\{.*?\})\s*</script>', html, re.S)
  94. imports = json.loads(m.group(1))['imports']
  95. v = re.search(r'three@(0\.\d+\.\d+)/', imports['three']).group(1)
  96. va = re.search(r'three@(0\.\d+\.\d+)/', imports['three/addons/']).group(1)
  97. assert v == va, f'{v} != {va}'
  98. " "$here/assets/importmap-starter.html" && ok "starter import map pinned + consistent" || bad "starter import map broken"
  99. # 8. Negative test: verifier catches a broken facts file (exit 4 VALIDATION)
  100. tmpd="$(mktemp -d)"
  101. cp -r "$here/scripts" "$here/assets" "$here/references" "$tmpd/"
  102. cp "$skill" "$tmpd/SKILL.md"
  103. "$PY" -c "
  104. import json, sys
  105. p = sys.argv[1]
  106. d = json.load(open(p, encoding='utf-8'))
  107. d['version_gates']['umd_builds_removed'] = 'r999' # gate no longer stated in SKILL.md
  108. json.dump(d, open(p, 'w', encoding='utf-8'))
  109. " "$tmpd/assets/three-facts.json"
  110. "$PY" "$tmpd/scripts/check-three-facts.py" --offline >/dev/null 2>&1
  111. [ "$?" -eq 4 ] && ok "negative: broken gate → exit 4 (VALIDATION)" || bad "negative: broken gate not caught"
  112. rm -rf "$tmpd"
  113. echo "threejs-ops self-test: $pass passed, $fail failed" >&2
  114. [ "$fail" -eq 0 ]