#!/usr/bin/env bash # Tests unitaires pour install.sh # Usage : cd tests && bash test_install.sh set -euo pipefail PASS=0 FAIL=0 assert_eq() { local desc="$1" expected="$2" actual="$3" if [[ "$expected" == "$actual" ]]; then echo " ✓ $desc" (( PASS++ )) || true else echo " ✗ $desc" echo " attendu : '$expected'" echo " obtenu : '$actual'" (( FAIL++ )) || true fi } assert_true() { local desc="$1" shift if eval "$@" 2>/dev/null; then echo " ✓ $desc" (( PASS++ )) || true else echo " ✗ $desc" (( FAIL++ )) || true fi } assert_false() { local desc="$1" shift if ! eval "$@" 2>/dev/null; then echo " ✓ $desc" (( PASS++ )) || true else echo " ✗ $desc" (( FAIL++ )) || true fi } # Charge install.sh sans exécuter main() load_install() { local tmp; tmp=$(mktemp) # Supprime l'appel final à main et desactive set -e pour le sourcing grep -v '^main "\$@"' ../install.sh > "$tmp" set +e # shellcheck disable=SC1090 source "$tmp" set -e rm -f "$tmp" } echo "" echo "══════════════════════════════════" echo " Tests install.sh" echo "══════════════════════════════════" echo "" load_install # Désactive le trap cleanup() hérité de install.sh (évite l'erreur nounset sur STATE_FILE) trap - EXIT # ── 1. Comparaison de versions ──────────────────────────────────── echo "1. version_is_newer()" assert_true "1.2.0 > 1.0.0" "version_is_newer '1.0.0' '1.2.0'" assert_false "1.0.0 n'est pas > 1.2.0" "version_is_newer '1.2.0' '1.0.0'" assert_false "1.0.0 == 1.0.0 → false" "version_is_newer '1.0.0' '1.0.0'" assert_true "2.0.0 > 1.9.9" "version_is_newer '1.9.9' '2.0.0'" assert_true "1.0.1 > 1.0.0" "version_is_newer '1.0.0' '1.0.1'" # ── 2. Parsing frontmatter ──────────────────────────────────────── echo "" echo "2. get_frontmatter_field()" TMP_SKILL=$(mktemp --suffix=.md) cat > "$TMP_SKILL" << 'SKILL_EOF' --- name: test-skill version: 2.1.0 description: Skill de test pour vérification agents: [claude-code] category: dev tags: [bash, test] --- # Contenu du skill SKILL_EOF assert_eq "version" "2.1.0" "$(get_frontmatter_field "$TMP_SKILL" "version")" assert_eq "name" "test-skill" "$(get_frontmatter_field "$TMP_SKILL" "name")" assert_eq "category" "dev" "$(get_frontmatter_field "$TMP_SKILL" "category")" assert_eq "champ absent → vide" "" "$(get_frontmatter_field "$TMP_SKILL" "nonexistent")" rm -f "$TMP_SKILL" # ── 3. Chemins de destination ───────────────────────────────────── echo "" echo "3. get_dest_path()" assert_eq "claude global" \ "$HOME/.claude/skills/dev/debugging/SKILL.md" \ "$(get_dest_path "dev" "debugging" "claude-code" "global")" assert_eq "claude local" \ ".claude/skills/dev/debugging/SKILL.md" \ "$(get_dest_path "dev" "debugging" "claude-code" "local")" assert_eq "gemini local" \ ".gemini/skills/dev/debugging/SKILL.md" \ "$(get_dest_path "dev" "debugging" "gemini-cli" "local")" assert_eq "codex global" \ "$HOME/.codex/skills/jardinage/arrosage/SKILL.md" \ "$(get_dest_path "jardinage" "arrosage" "codex" "global")" assert_eq "hermes local" \ ".hermes/skills/electronique/arduino/SKILL.md" \ "$(get_dest_path "electronique" "arduino" "hermes" "local")" # ── 4. Gestion d'état ───────────────────────────────────────────── echo "" echo "4. state_get() / state_cycle() / make_key()" STATE_FILE=$(mktemp) export STATE_FILE # Teste make_key — prend une entrée "cat|skill|agent|etat|repo_ver|local_ver" assert_eq "make_key basique" \ "dev_debugging_claude_code" \ "$(make_key "dev|debugging|claude-code|new|1.0.0|")" assert_eq "make_key avec tirets" \ "dev_git_workflow_claude_code" \ "$(make_key "dev|git-workflow|claude-code|new|1.0.0|")" # Initialise un état echo "dev_debugging_claude_code=local" > "$STATE_FILE" assert_eq "state_get local" "local" "$(state_get "dev_debugging_claude_code")" state_cycle "dev_debugging_claude_code" "ok" assert_eq "cycle local→global" "global" "$(state_get "dev_debugging_claude_code")" state_cycle "dev_debugging_claude_code" "ok" assert_eq "cycle global→skip" "skip" "$(state_get "dev_debugging_claude_code")" state_cycle "dev_debugging_claude_code" "ok" assert_eq "cycle skip→local (état ok)" "local" "$(state_get "dev_debugging_claude_code")" # Teste le cycle avec état upd echo "dev_debugging_claude_code=skip" > "$STATE_FILE" state_cycle "dev_debugging_claude_code" "upd" assert_eq "cycle skip→update (état upd)" "update" "$(state_get "dev_debugging_claude_code")" state_cycle "dev_debugging_claude_code" "upd" assert_eq "cycle update→local" "local" "$(state_get "dev_debugging_claude_code")" rm -f "$STATE_FILE" unset STATE_FILE || true # ── 5. Détection bundle / legacy ───────────────────────────────── echo "" echo "5. get_frontmatter_agents() / scan_skills() double format" TMP_DIR=$(mktemp -d) # Skill bundle : SKILL.md + agents: [claude-code, codex] mkdir -p "$TMP_DIR/skills/infra/mon-bundle" cat > "$TMP_DIR/skills/infra/mon-bundle/SKILL.md" << 'EOF' --- name: mon-bundle version: 1.2.0 agents: [claude-code, codex] description: Skill bundle test tags: [test] --- # Mon bundle EOF mkdir -p "$TMP_DIR/skills/infra/mon-bundle/scripts" touch "$TMP_DIR/skills/infra/mon-bundle/scripts/helper.sh" # Skill legacy : claude-code.md mkdir -p "$TMP_DIR/skills/dev/ancien" cat > "$TMP_DIR/skills/dev/ancien/claude-code.md" << 'EOF' --- name: ancien version: 0.5.0 description: Skill legacy tags: [legacy] --- # Ancien skill EOF # Fichier Markdown de support (ne doit PAS être détecté comme skill) mkdir -p "$TMP_DIR/skills/infra/mon-bundle/references" echo "# Ref doc" > "$TMP_DIR/skills/infra/mon-bundle/references/notes.md" assert_eq "agents: [claude-code, codex] → 2 agents" \ "claude-code codex" \ "$(get_frontmatter_agents "$TMP_DIR/skills/infra/mon-bundle/SKILL.md")" assert_eq "agents: absent → vide" \ "" \ "$(get_frontmatter_agents "$TMP_DIR/skills/dev/ancien/claude-code.md")" # scan_skills avec les deux formats REPO_DIR="$TMP_DIR" DETECTED_AGENTS=() SKILLS_TAG="" STATE_FILE=$(mktemp) scan_skills 2>/dev/null # Compter les entrées trouvées bundle_count=0; legacy_count=0; ref_count=0 for e in "${SKILLS_LIST[@]}"; do IFS='|' read -r _ _ _ _ _ _ _ _ kind source_path <<< "$e" [[ "$kind" == "bundle" ]] && (( bundle_count++ )) || true [[ "$kind" == "legacy" ]] && (( legacy_count++ )) || true # Vérifier qu'aucune entrée ne vient de references/notes.md [[ "$source_path" == *"notes.md"* ]] && (( ref_count++ )) || true done assert_eq "bundle détecté 2 fois (2 agents)" "2" "$bundle_count" assert_eq "legacy détecté 1 fois" "1" "$legacy_count" assert_eq "fichier references/ ignoré" "0" "$ref_count" # Vérifier que kind=bundle et source_path pointent sur le dossier first_bundle="" for e in "${SKILLS_LIST[@]}"; do IFS='|' read -r _ _ _ _ _ _ _ _ kind source_path <<< "$e" [[ "$kind" == "bundle" ]] && { first_bundle="$source_path"; break; } done assert_eq "source_path bundle = dossier du skill" \ "$TMP_DIR/skills/infra/mon-bundle" \ "$first_bundle" # Vérifier que SKILL.md du bundle est lisible pour le preview assert_true "SKILL.md bundle accessible" "[[ -f '${first_bundle}/SKILL.md' ]]" assert_true "scripts/ du bundle accessible" "[[ -f '${first_bundle}/scripts/helper.sh' ]]" # Priorité bundle sur legacy : si SKILL.md existe, le .md ne duplique pas mkdir -p "$TMP_DIR/skills/dev/ancien" cat > "$TMP_DIR/skills/dev/ancien/SKILL.md" << 'EOF' --- name: ancien version: 1.0.0 agents: [claude-code] description: Skill migré en bundle tags: [test] --- # Ancien migré EOF SKILLS_LIST=() scan_skills 2>/dev/null count_ancien=0 for e in "${SKILLS_LIST[@]}"; do [[ "$e" == *"|ancien|claude-code|"* ]] && (( count_ancien++ )) || true done assert_eq "pas de doublon bundle+legacy pour même agent" "1" "$count_ancien" rm -f "$STATE_FILE" rm -rf "$TMP_DIR" unset REPO_DIR STATE_FILE SKILLS_TAG DETECTED_AGENTS # ── Bilan ───────────────────────────────────────────────────────── echo "" echo "══════════════════════════════════" printf " Résultats : %d passés, %d échoués\n" "$PASS" "$FAIL" echo "══════════════════════════════════" echo "" [[ "$FAIL" -eq 0 ]]