203 lines
7.9 KiB
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"
|
|
''
|