Files
skills/checks/home-manager-module.nix

203 lines
7.9 KiB
Nix

# 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"
''