Files
skills/checks/home-manager-module.nix
alexion b0a763e5ce feat: add global-placement home-manager module (task 0002)
Expose homeModules.default, a home-manager module whose programs.agents.skills
option places operator-selected skill derivations into Claude Code's skills
directory, one recursive home.file per skill, gated on programs.claude-code.enable
and sourcing configDir from the claude-code module. Add a nix flake check that
builds the home-files tree under several operator configurations and asserts
placement, recursion, the enable gate, configDir sourcing, and coexistence with
an operator's own skills.
2026-07-22 17:18:05 -04:00

155 lines
5.7 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 ];
};
# Claude Code disabled, with a skill still selected.
claudeCodeOff = homeFiles {
programs.claude-code.enable = false;
programs.agents.skills = [ skillAlpha ];
};
# An empty selection with Claude Code enabled.
emptySelection = homeFiles {
programs.claude-code.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
claudeCodeOff
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 "Claude Code disabled: no skill entry is written despite a selection"
test ! -e "$claudeCodeOff/.claude/skills/sample-alpha" \
|| fail "a skill was placed with claude-code disabled"
echo "empty selection: nothing is installed"
test ! -e "$emptySelection/.claude/skills/sample-alpha" \
|| fail "an unselected skill was placed"
touch "$out"
''