run.sh 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. #!/usr/bin/env bash
  2. # Offline self-test for the migrate-ops skill — structure, frontmatter, and the
  3. # staleness-verifier contract (SKILL-RESOURCE-PROTOCOL §7, §10).
  4. #
  5. # Usage: tests/run.sh
  6. # Input: none (self-contained; no network)
  7. # Output: TAP-ish progress on stderr; final PASS/FAIL line.
  8. # Exit: 0 all pass (or skipped on unsupported platform), 1 any failure.
  9. #
  10. # Examples:
  11. # tests/run.sh
  12. # bash skills/migrate-ops/tests/run.sh
  13. set -uo pipefail
  14. here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
  15. fail=0
  16. pass=0
  17. note() { printf ' %s %s\n' "$1" "$2" >&2; }
  18. ok() { pass=$((pass+1)); note "ok " "$1"; }
  19. bad() { fail=$((fail+1)); note "FAIL" "$1"; }
  20. # Resolve a *working* python (python3, else python). The bare `command -v` is not
  21. # enough on Windows, where `python3` is a Microsoft Store stub that exits nonzero.
  22. PY=""
  23. for cand in python3 python; do
  24. if command -v "$cand" >/dev/null 2>&1 && "$cand" --version >/dev/null 2>&1; then
  25. PY="$cand"; break
  26. fi
  27. done
  28. if [ -z "$PY" ]; then
  29. echo "SKIP: no working python interpreter on this platform" >&2
  30. exit 0
  31. fi
  32. # 1. Required directories exist
  33. for d in scripts references assets tests; do
  34. [ -d "$here/$d" ] && ok "dir $d/ exists" || bad "missing dir $d/"
  35. done
  36. # 2. SKILL.md frontmatter house rules
  37. skill="$here/SKILL.md"
  38. if [ -f "$skill" ]; then
  39. ok "SKILL.md present"
  40. grep -q '^name: migrate-ops$' "$skill" && ok "name matches directory" || bad "name != migrate-ops"
  41. grep -q '^license: MIT$' "$skill" && ok "license: MIT" || bad "missing license: MIT"
  42. grep -q '^ author: claude-mods$' "$skill" && ok "metadata.author" || bad "missing metadata.author"
  43. else
  44. bad "SKILL.md missing"
  45. fi
  46. # 3. Every reference on disk is cited from SKILL.md (no dead weight)
  47. for ref in "$here"/references/*.md; do
  48. base="references/$(basename "$ref")"
  49. grep -qF "$base" "$skill" && ok "cited: $base" || bad "uncited reference: $base"
  50. done
  51. # 4. Every SKILL.md-cited bundled resource exists on disk
  52. for res in assets/migrate-facts.json scripts/check-migrate-facts.py; do
  53. [ -f "$here/$res" ] && ok "resource present: $res" || bad "missing resource: $res"
  54. done
  55. # 5. check-migrate-facts.py — staleness verifier contract (§7, §10), offline-safe
  56. verifier="$here/scripts/check-migrate-facts.py"
  57. catalog="$here/assets/migrate-facts.json"
  58. ec() { local want="$1" lbl="$2"; shift 2; "$@" >/dev/null 2>&1; local got=$?
  59. [ "$got" = "$want" ] && ok "$lbl (exit $got)" || bad "$lbl (want $want got $got)"; }
  60. if [ -f "$verifier" ]; then
  61. "$PY" -m py_compile "$verifier" && ok "verifier: py_compile clean" || bad "verifier: py_compile failed"
  62. grep -qE '^Examples:$' "$verifier" && ok "verifier: has Examples block" || bad "verifier: no Examples block (docstring)"
  63. "$PY" "$verifier" --help >/dev/null 2>&1 && ok "verifier: --help exits 0" || bad "verifier: --help nonzero"
  64. # Offline mode must pass on the skill's own content (internal consistency).
  65. ec 0 "verifier: --offline consistent" "$PY" "$verifier" --offline
  66. # Bad flag → USAGE (exit 2); conflicting modes → USAGE.
  67. ec 2 "verifier: bad flag → exit 2" "$PY" "$verifier" --bogus
  68. ec 2 "verifier: --offline --live → 2" "$PY" "$verifier" --offline --live
  69. # stdout is data-only: --offline --json must emit parseable JSON.
  70. "$PY" "$verifier" --offline --json -q 2>/dev/null \
  71. | "$PY" -c 'import json,sys; d=json.load(sys.stdin); assert d["meta"]["schema"]=="claude-mods.migrate-ops.facts/v1"' \
  72. && ok "verifier: --json envelope parses (stdout clean)" || bad "verifier: --json envelope broken"
  73. # Error paths: missing catalog → 3, malformed catalog → 4, drift catalog → 10.
  74. TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
  75. ec 3 "verifier: missing catalog -> 3" "$PY" "$verifier" --offline --catalog "$TMP/nope.json"
  76. printf '{"packages":"x"}' > "$TMP/bad.json"
  77. ec 4 "verifier: malformed catalog -> 4" "$PY" "$verifier" --offline --catalog "$TMP/bad.json"
  78. # Minimal drift catalog (argv path so MSYS translates it): a claim whose
  79. # pattern matches nothing in the real skill prose -> drift -> exit 10.
  80. printf '%s\n' '{"schema":"claude-mods.migrate-ops.facts/v1","as_of":"2026-07-05","claims":[{"label":"Fakeweb","version":"99","where":["description","body"],"pattern":"fakeweb[^\\n]*\\b99\\b"}]}' > "$TMP/drift.json"
  81. ec 10 "verifier: missing claim -> 10" "$PY" "$verifier" --offline --catalog "$TMP/drift.json"
  82. # cited from SKILL.md
  83. grep -qF "scripts/check-migrate-facts.py" "$skill" && ok "verifier: cited from SKILL.md" || bad "verifier: uncited"
  84. else
  85. bad "check-migrate-facts.py missing"
  86. fi
  87. # 6. migrate-facts.json — parses, carries schema + the 8 catalogued targets
  88. "$PY" -c "
  89. import json, sys
  90. d = json.load(open(sys.argv[1], encoding='utf-8'))
  91. assert d['schema'] == 'claude-mods.migrate-ops.facts/v1'
  92. labels = {c['label'] for c in d['claims']}
  93. for need in ['React','Laravel','Python','Node','TypeScript','Go','Rust','PHP']:
  94. assert need in labels, f'missing claim {need}'
  95. assert all(c['version'] for c in d['claims']), 'claim missing version'
  96. " "$catalog" && ok "migrate-facts.json schema + 8 targets" || bad "migrate-facts.json invalid"
  97. # 7. Currency note present near the top of the body
  98. grep -qE 'verified as of [0-9]{4}' "$skill" && ok "currency note present" || bad "no dated currency note"
  99. echo "migrate-ops self-test: $pass passed, $fail failed" >&2
  100. [ "$fail" -eq 0 ]