# Builds the home-files tree `homeModules.default` produces under several # operator configurations and asserts on the skills each one places. { pkgs, home-manager, module, mkSkill, }: let inherit (pkgs) lib; # A sample skill built through the flake's own `mkSkill`, so it carries the # `skillName` passthru the module reads to place it. 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"; # A skill the operator declares as their own attribute-set entry, so the # attribute-set assertion can look for it beside the module's. operatorAttrSkill = pkgs.runCommandLocal "operator-attr-skill" { } '' mkdir -p "$out" printf '%s\n' "the operator's own attribute-set skill" > "$out/SKILL.md" ''; # A whole directory of skills, one folder per skill, for the path form of the # operator's own `programs.claude-code.skills`. operatorSkillsDir = pkgs.runCommandLocal "operator-skills-dir" { } '' mkdir -p "$out/operator-dir-skill" printf '%s\n' "the operator's own whole-directory skill" \ > "$out/operator-dir-skill/SKILL.md" ''; # Evaluate the real module through home-manager's standalone entry point and # return the home-files tree it would link into $HOME. homeFiles = operatorConfig: (home-manager.lib.homeManagerConfiguration { inherit pkgs; modules = [ module { home.username = "operator"; home.homeDirectory = "/home/operator"; home.stateVersion = "24.11"; } operatorConfig ]; }).config.home-files; # Two skills selected beside the operator's own attribute-set skill, with # Claude Code enabled. attrSetSkills = homeFiles { programs.claude-code.enable = true; programs.agents.skills = [ skillAlpha skillBeta ]; # A store path, not a bare derivation, so the value takes the option's path # branch rather than its attribute-set branch. programs.claude-code.skills.operator-attr-skill = "${operatorAttrSkill}"; }; # The operator's own skills as a whole directory (path form). # Setting this single-valued option to a path also proves the module never # contributes to it, since an attribute-set contribution would clash with the # path at eval. wholeDirSkills = homeFiles { programs.claude-code.enable = true; programs.agents.skills = [ skillAlpha ]; programs.claude-code.skills = "${operatorSkillsDir}"; }; # A non-default `configDir`, so the assertions can prove placement follows it # rather than a hardcoded `.claude`. customConfigDir = homeFiles { programs.claude-code.enable = true; programs.claude-code.configDir = "/home/operator/.config/claude"; programs.agents.skills = [ skillAlpha ]; }; # Pi enabled and Claude Code disabled. piOnly = homeFiles { programs.pi-coding-agent.enable = true; programs.agents.skills = [ skillAlpha ]; }; # Both supported harnesses enabled. bothHarnesses = homeFiles { programs.claude-code.enable = true; programs.pi-coding-agent.enable = true; programs.agents.skills = [ skillAlpha ]; }; # A non-default Pi `configDir`, so the assertions can prove placement follows it # rather than a hardcoded `.pi/agent`. customPiConfigDir = homeFiles { programs.pi-coding-agent.enable = true; programs.pi-coding-agent.configDir = "/home/operator/.config/pi/agent"; programs.agents.skills = [ skillAlpha ]; }; # No compatible harness enabled, with a skill still selected. agentsOff = homeFiles { programs.claude-code.enable = false; programs.pi-coding-agent.enable = false; programs.agents.skills = [ skillAlpha ]; }; # An empty selection with both supported harnesses enabled. emptySelection = homeFiles { programs.claude-code.enable = true; programs.pi-coding-agent.enable = true; programs.agents.skills = [ ]; }; in pkgs.runCommandLocal "skills-home-manager-module-check" { # Forcing each derivation as a build input builds its home-files tree, so an # eval-level composition failure surfaces before any assertion runs. inherit attrSetSkills wholeDirSkills customConfigDir piOnly bothHarnesses customPiConfigDir agentsOff emptySelection ; } '' fail() { echo "FAIL: $1" >&2; exit 1; } echo "attribute-set skills: both selected skills and the operator's own land" test -f "$attrSetSkills/.claude/skills/sample-alpha/SKILL.md" \ || fail "selected skill sample-alpha did not land at its own name" test -f "$attrSetSkills/.claude/skills/sample-beta/SKILL.md" \ || fail "selected skill sample-beta did not land at its own name" test -f "$attrSetSkills/.claude/skills/operator-attr-skill/SKILL.md" \ || fail "the operator's own attribute-set skill was clobbered" echo "recursive placement: a skill is a real directory of per-file symlinks" # A file-existence test follows symlinks, so it cannot tell a recursive tree # from one opaque symlink over the whole skill. # The entry's own type is what the assertion reads instead. test -d "$attrSetSkills/.claude/skills/sample-alpha" \ && test ! -L "$attrSetSkills/.claude/skills/sample-alpha" \ || fail "sample-alpha was placed as an opaque symlink, not a recursive tree" echo "whole-directory skills: the selected skill lands beside the operator's directory" test -f "$wholeDirSkills/.claude/skills/sample-alpha/SKILL.md" \ || fail "selected skill did not coexist with a whole skills directory" test -f "$wholeDirSkills/.claude/skills/operator-dir-skill/SKILL.md" \ || fail "the operator's whole skills directory was clobbered" echo "custom configDir: placement follows configDir rather than a hardcoded .claude" test -f "$customConfigDir/.config/claude/skills/sample-alpha/SKILL.md" \ || fail "skill was not placed under the configured configDir" test ! -e "$customConfigDir/.claude/skills/sample-alpha" \ || fail "skill was placed under a hardcoded .claude, ignoring configDir" echo "Pi enabled: selected skills land in Pi's global skills directory" test -f "$piOnly/.pi/agent/skills/sample-alpha/SKILL.md" \ || fail "selected skill did not land under Pi's configDir" test ! -e "$piOnly/.claude/skills/sample-alpha" \ || fail "a skill was placed for Claude Code when only Pi was enabled" echo "both harnesses enabled: selected skills land in both global skills directories" test -f "$bothHarnesses/.claude/skills/sample-alpha/SKILL.md" \ || fail "selected skill did not land under Claude Code when both harnesses were enabled" test -f "$bothHarnesses/.pi/agent/skills/sample-alpha/SKILL.md" \ || fail "selected skill did not land under Pi when both harnesses were enabled" echo "custom Pi configDir: placement follows configDir rather than a hardcoded .pi/agent" test -f "$customPiConfigDir/.config/pi/agent/skills/sample-alpha/SKILL.md" \ || fail "skill was not placed under Pi's configured configDir" test ! -e "$customPiConfigDir/.pi/agent/skills/sample-alpha" \ || fail "skill was placed under a hardcoded .pi/agent, ignoring configDir" echo "agents disabled: no skill entry is written despite a selection" test ! -e "$agentsOff/.claude/skills/sample-alpha" \ || fail "a skill was placed with Claude Code disabled" test ! -e "$agentsOff/.pi/agent/skills/sample-alpha" \ || fail "a skill was placed with Pi disabled" echo "empty selection: nothing is installed" test ! -e "$emptySelection/.claude/skills/sample-alpha" \ || fail "an unselected skill was placed for Claude Code" test ! -e "$emptySelection/.pi/agent/skills/sample-alpha" \ || fail "an unselected skill was placed for Pi" touch "$out" ''