diff --git a/bin/tests/test-deploy-skill.sh b/bin/tests/test-deploy-skill.sh new file mode 100755 index 0000000..6e00c9d --- /dev/null +++ b/bin/tests/test-deploy-skill.sh @@ -0,0 +1,299 @@ +#!/usr/bin/env bash +# Tests for deploy-skill.sh config injection functions +# Run: bash bin/tests/test-deploy-skill.sh + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PASSED=0 +FAILED=0 + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' # No Color + +# Extract inject_nix_config function +eval "$(sed -n '/^inject_nix_config()/,/^}/p' "$SCRIPT_DIR/../deploy-skill.sh")" + +# Extract inject_home_file function +eval "$(sed -n '/^inject_home_file()/,/^}/p' "$SCRIPT_DIR/../deploy-skill.sh")" + +# Test helpers +assert_contains() { + local description="$1" + local pattern="$2" + local file="$3" + + if grep -q "$pattern" "$file" 2>/dev/null; then + echo -e "${GREEN}PASS${NC}: $description" + ((PASSED++)) + else + echo -e "${RED}FAIL${NC}: $description" + echo " Pattern not found: '$pattern'" + echo " File contents:" + cat "$file" | sed 's/^/ /' + ((FAILED++)) + fi +} + +assert_not_contains() { + local description="$1" + local pattern="$2" + local file="$3" + + if ! grep -q "$pattern" "$file" 2>/dev/null; then + echo -e "${GREEN}PASS${NC}: $description" + ((PASSED++)) + else + echo -e "${RED}FAIL${NC}: $description" + echo " Pattern found but should not be: '$pattern'" + ((FAILED++)) + fi +} + +assert_count() { + local description="$1" + local expected="$2" + local pattern="$3" + local file="$4" + + local actual + actual=$(grep -c "$pattern" "$file" 2>/dev/null || echo "0") + + if [[ "$actual" == "$expected" ]]; then + echo -e "${GREEN}PASS${NC}: $description" + ((PASSED++)) + else + echo -e "${RED}FAIL${NC}: $description" + echo " Expected count: $expected, Actual: $actual" + echo " Pattern: '$pattern'" + ((FAILED++)) + fi +} + +assert_last_line() { + local description="$1" + local expected="$2" + local file="$3" + + local actual + actual=$(tail -n 1 "$file") + + if [[ "$actual" == "$expected" ]]; then + echo -e "${GREEN}PASS${NC}: $description" + ((PASSED++)) + else + echo -e "${RED}FAIL${NC}: $description" + echo " Expected last line: '$expected'" + echo " Actual last line: '$actual'" + ((FAILED++)) + fi +} + +assert_output_contains() { + local description="$1" + local pattern="$2" + local output="$3" + + if echo "$output" | grep -q "$pattern"; then + echo -e "${GREEN}PASS${NC}: $description" + ((PASSED++)) + else + echo -e "${RED}FAIL${NC}: $description" + echo " Pattern not found: '$pattern'" + echo " Output was: $output" + ((FAILED++)) + fi +} + +# Setup: create a minimal Nix file +setup_nix_file() { + local content="$1" + local test_file + test_file=$(mktemp) + echo -e "$content" > "$test_file" + echo "$test_file" +} + +cleanup() { + rm -f "$1" +} + +echo "=== Deploy Skill Config Injection Tests ===" +echo "" + +# --- Basic Injection --- +echo "## Basic Injection" + +# Test: Config injected before closing brace +test_file=$(setup_nix_file "{ config, pkgs, ... }: +{ + home.packages = [ pkgs.git ]; +}") + +inject_nix_config "$test_file" " # Added config + home.file.\"test\" = { source = ./test; };" "home.file.\"test\"" > /dev/null + +assert_contains "Config block injected" "home.file.\"test\"" "$test_file" +assert_last_line "Closing brace preserved at end" "}" "$test_file" +assert_contains "Original content preserved" "home.packages" "$test_file" + +cleanup "$test_file" + +# --- Idempotency --- +echo "" +echo "## Idempotency" + +# Test: Running twice doesn't duplicate +test_file=$(setup_nix_file "{ config, pkgs, ... }: +{ + home.packages = [ ]; +}") + +inject_nix_config "$test_file" " home.file.\"skill-a\" = { source = ./a; };" "skill-a" > /dev/null +inject_nix_config "$test_file" " home.file.\"skill-a\" = { source = ./a; };" "skill-a" > /dev/null + +assert_count "Config not duplicated" "1" "skill-a" "$test_file" + +cleanup "$test_file" + +# Test: Different configs can be added +test_file=$(setup_nix_file "{ config, pkgs, ... }: +{ +}") + +inject_nix_config "$test_file" " home.file.\"skill-b\" = {};" "skill-b" > /dev/null +inject_nix_config "$test_file" " home.file.\"skill-c\" = {};" "skill-c" > /dev/null + +assert_contains "First config present" "skill-b" "$test_file" +assert_contains "Second config present" "skill-c" "$test_file" +assert_count "Each config appears once" "1" "skill-b" "$test_file" +assert_count "Each config appears once (c)" "1" "skill-c" "$test_file" + +cleanup "$test_file" + +# --- File Not Found --- +echo "" +echo "## File Not Found Handling" + +output=$(inject_nix_config "/nonexistent/path/file.nix" "config" "marker" 2>&1) +assert_output_contains "Skips missing file gracefully" "skipping" "$output" + +# --- Brace Structure --- +echo "" +echo "## Brace Structure Preservation" + +# Test: Complex nested structure +test_file=$(setup_nix_file "{ config, pkgs, ... }: +{ + programs.git = { + enable = true; + userName = \"test\"; + }; + + home.packages = with pkgs; [ + ripgrep + fd + ]; +}") + +inject_nix_config "$test_file" " + # New skill + home.file.\".skill\" = { + source = ./skill; + recursive = true; + };" ".skill" > /dev/null + +assert_last_line "Closing brace still at end after complex inject" "}" "$test_file" +assert_contains "Nested braces preserved" "programs.git" "$test_file" +assert_contains "New config added" ".skill" "$test_file" + +cleanup "$test_file" + +# --- inject_home_file Wrapper --- +echo "" +echo "## inject_home_file Wrapper" + +test_file=$(setup_nix_file "{ config, pkgs, ... }: +{ +}") + +inject_home_file "$test_file" \ + ".claude/skills/my-skill" \ + "../claude/skills/my-skill" \ + "recursive = true;" \ + "my-skill" > /dev/null + +assert_contains "Home path in config" ".claude/skills/my-skill" "$test_file" +assert_contains "Source path in config" "../claude/skills/my-skill" "$test_file" +assert_contains "Extra props included" "recursive = true" "$test_file" +assert_contains "Comment included" "# Skill: my-skill" "$test_file" + +cleanup "$test_file" + +# Test: inject_home_file idempotency +test_file=$(setup_nix_file "{ config, pkgs, ... }: +{ +}") + +inject_home_file "$test_file" ".test/path" "./source" "" "test" > /dev/null +inject_home_file "$test_file" ".test/path" "./source" "" "test" > /dev/null + +assert_count "inject_home_file idempotent" "1" ".test/path" "$test_file" + +cleanup "$test_file" + +# --- Already Present Detection --- +echo "" +echo "## Already Present Detection" + +test_file=$(setup_nix_file "{ config, pkgs, ... }: +{ + # Existing skill + home.file.\".claude/skills/existing\" = { + source = ./existing; + }; +}") + +output=$(inject_nix_config "$test_file" "new config" ".claude/skills/existing" 2>&1) +assert_output_contains "Detects existing config" "already present" "$output" + +cleanup "$test_file" + +# --- Empty Extra Props --- +echo "" +echo "## Edge Cases" + +# Test: Empty extra props +test_file=$(setup_nix_file "{ config, pkgs, ... }: +{ +}") + +inject_home_file "$test_file" ".simple/path" "./src" "" "simple" > /dev/null + +assert_contains "Works with empty extra props" ".simple/path" "$test_file" + +cleanup "$test_file" + +# Test: Single line file (edge case - script assumes multi-line) +test_file=$(setup_nix_file "{}") + +inject_nix_config "$test_file" " config = true;" "config" > /dev/null + +assert_contains "Works with minimal file" "config = true" "$test_file" +# Note: Single-line "{}" becomes last line since head -n -1 returns empty +# This is acceptable - real Nix files are always multi-line +assert_last_line "Original content preserved as last line" "{}" "$test_file" + +cleanup "$test_file" + +# --- Summary --- +echo "" +echo "=== Summary ===" +echo -e "Passed: ${GREEN}$PASSED${NC}" +echo -e "Failed: ${RED}$FAILED${NC}" + +if [[ $FAILED -gt 0 ]]; then + exit 1 +fi