# Runs the dev-shell helper's hook against a fixture project and asserts the # placement, stateless stale-removal, and hand-authored-skill safety it promises. { pkgs, mkSkill, mkSkillsShellHook, }: let # Sample skills built through the flake's own `mkSkill`, so they carry the # `skillName` passthru the hook reads and land at real store paths the hook # can link to. mkSampleSkill = name: mkSkill { inherit pkgs name; src = pkgs.runCommandLocal "${name}-src" { } '' mkdir -p "$out" printf '%s\n' "sample skill ${name}" > "$out/SKILL.md" ''; }; skillAlpha = mkSampleSkill "sample-alpha"; skillBeta = mkSampleSkill "sample-beta"; # Selected, but a hand-authored real directory of the same name already exists # in the fixture, so the hook must skip it. skillGamma = mkSampleSkill "sample-gamma"; # Two hooks for two successive selections: the second drops sample-beta, so # sourcing it after the first proves a deselected skill's symlink is removed. hookBoth = mkSkillsShellHook [ skillAlpha skillBeta skillGamma ]; hookAlpha = mkSkillsShellHook [ skillAlpha ]; in pkgs.runCommandLocal "skills-shell-hook-check" { inherit hookBoth hookAlpha; alphaPath = "${skillAlpha}"; betaPath = "${skillBeta}"; } '' fail() { echo "FAIL: $1" >&2; exit 1; } echo "the hook is a non-empty string referencing each skill's store path and name" [ -n "$hookBoth" ] || fail "the produced hook is empty" for token in "$alphaPath" "$betaPath" sample-alpha sample-beta; do printf '%s' "$hookBoth" | grep -qF "$token" \ || fail "the hook does not reference $token" done # The fixture project carries three things the hook must not disturb: a # hand-authored skill under its own name, a hand-authored skill whose name # collides with a selected one, and a symlink pointing outside the store. mkdir -p proj/.agents/skills/handmade printf '%s\n' "a hand-authored skill" > proj/.agents/skills/handmade/SKILL.md printf '%s\n' "# fixture ignore" "result" > proj/.gitignore mkdir -p proj/.agents/skills/sample-gamma printf '%s\n' "a hand-authored gamma" > proj/.agents/skills/sample-gamma/SKILL.md ln -s /nonexistent/external-target proj/.agents/skills/external cd proj echo "first entry: the two free skills link as store symlinks that resolve" printf '%s\n' "$hookBoth" > ../hook-both.sh . ../hook-both.sh for name in sample-alpha sample-beta; do [ -L ".agents/skills/$name" ] || fail "$name was not linked as a symlink" case "$(readlink ".agents/skills/$name")" in ${builtins.storeDir}/*) : ;; *) fail "$name's symlink does not point into the store" ;; esac test -f ".agents/skills/$name/SKILL.md" \ || fail "$name's link does not resolve to its SKILL.md" done echo "a selected skill colliding with a hand-authored one leaves the real directory" { [ -d .agents/skills/sample-gamma ] && [ ! -L .agents/skills/sample-gamma ]; } \ || fail "the colliding hand-authored skill was replaced by a symlink" grep -qx "a hand-authored gamma" .agents/skills/sample-gamma/SKILL.md \ || fail "the colliding hand-authored skill's content was overwritten" echo "the non-colliding hand-authored skill is an untouched real directory" { [ -d .agents/skills/handmade ] && [ ! -L .agents/skills/handmade ]; } \ || fail "the hand-authored skill was replaced or removed" test -f .agents/skills/handmade/SKILL.md || fail "the hand-authored skill lost its content" echo "a symlink pointing outside the store is not ours, so it is left alone" [ -L .agents/skills/external ] || fail "the non-store symlink was removed" echo "Claude Code compatibility points at the canonical skill directory" [ -L .claude/skills ] || fail ".claude/skills is not a compatibility symlink" [ "$(readlink .claude/skills)" = "../.agents/skills" ] \ || fail ".claude/skills points somewhere other than ../.agents/skills" test -f .claude/skills/sample-alpha/SKILL.md \ || fail "Claude Code compatibility link does not resolve selected skills" echo "the project .gitignore lists only generated links and keeps skill scanners clear" test ! -e .agents/skills/.gitignore || fail "the hook wrote an ignore file where skill scanners read it" grep -qx '# fixture ignore' .gitignore || fail "the existing .gitignore content was lost" grep -qx 'result' .gitignore || fail "the existing .gitignore pattern was lost" grep -qx '# BEGIN mkSkillsShellHook' .gitignore || fail ".gitignore lacks the managed block start" grep -qx '# END mkSkillsShellHook' .gitignore || fail ".gitignore lacks the managed block end" grep -qx '.claude/skills' .gitignore || fail ".gitignore omits the compatibility symlink" grep -qx '.agents/skills/sample-alpha' .gitignore || fail ".gitignore omits sample-alpha" grep -qx '.agents/skills/sample-beta' .gitignore || fail ".gitignore omits sample-beta" for tracked in .agents/skills/handmade .agents/skills/sample-gamma .agents/skills/external; do grep -qx "$tracked" .gitignore \ && fail ".gitignore lists $tracked, which would untrack a real path" done echo "no manifest or state file is written: only the fixtures and links exist" entries=$(ls -A .agents/skills | sort | tr '\n' ' ') [ "$entries" = "external handmade sample-alpha sample-beta sample-gamma " ] \ || fail "unexpected entries under .agents/skills: [$entries]" echo "second entry with sample-beta deselected: its symlink is removed, the rest stay" printf '%s\n' "$hookAlpha" > ../hook-alpha.sh . ../hook-alpha.sh test ! -e .agents/skills/sample-beta || fail "the deselected skill's symlink was not removed" [ -L .agents/skills/sample-alpha ] || fail "the still-selected skill was dropped" { [ -d .agents/skills/handmade ] && [ ! -L .agents/skills/handmade ]; } \ || fail "the hand-authored skill was disturbed on re-entry" { [ -d .agents/skills/sample-gamma ] && [ ! -L .agents/skills/sample-gamma ]; } \ || fail "the colliding hand-authored skill was disturbed on re-entry" [ -L .agents/skills/external ] || fail "the non-store symlink was disturbed on re-entry" echo "the .gitignore tracks the new selection without duplicating the managed block" grep -qx '.agents/skills/sample-alpha' .gitignore || fail ".gitignore lost sample-alpha" grep -qx '.agents/skills/sample-beta' .gitignore \ && fail ".gitignore still lists the deselected sample-beta" [ "$(grep -xc '# BEGIN mkSkillsShellHook' .gitignore)" = 1 ] \ || fail ".gitignore has duplicate managed blocks" touch "$out" ''