From 1e2d1b940e8ea313c448cd60ceb75cbe66662c59 Mon Sep 17 00:00:00 2001 From: Sergio Romero Date: Wed, 2 Sep 2026 19:48:48 -0600 Subject: [PATCH] test(install): add a regression suite for install.sh + CI on Linux and macOS (#772) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(install): add a regression suite for install.sh + CI on Linux and macOS install.sh is the largest script in the repo and has no tests. Every install bug so far has been a silent one — agents copied to the wrong directory, a path with a space split in two, a filter that installed everything — and the only signal was a user noticing later. scripts/test-install.sh pins the installer's observable contract: * destinations: default $HOME/.claude/agents, --path override, tool env var override, and --path winning over the env var * selection: --division, --agent, --agents-file (comments/blank lines) * --dry-run writes nothing; unknown --tool exits non-zero * --link produces symlinks; a second run installs the same set, not dupes * a destination containing spaces stays one directory Expected counts are derived from divisions.json + lib.sh at runtime, so the suite doesn't need updating when agents are added. Every case runs with HOME pointed at a throwaway sandbox, so a broken default path can never write into the real config. bash 3.2 + BSD userland, no new dependencies. Verified it fails on the regressions it claims to catch: unquoting install_file fails only the spaces case, neutering slug_allowed fails the four selection cases, un-short-circuiting --dry-run fails the dry-run case, and ignoring the env var in resolve_dest fails the env-override case. CI runs it on ubuntu-latest and macos-latest (macOS ships bash 3.2, Linux ships bash 5) plus bash -n over every script in scripts/. * test(install): pin the parallel worker argument regression (#755) Review feedback: the existing "paths with spaces" case selects a single tool, so it stays on the serial path and never reaches the worker spawn where #755's bug lives. Adds a case that does. --tool claude-code,copilot --parallel --jobs 1 --agents-file --path "/My [Agents]/dest dir" with a serial control immediately before it (same two tools, same spaced and globbed --path, no --parallel) so a failure is attributable to the worker hand-off rather than to the selection filter. Marked xfail rather than a hard assertion: it fails on main today and passes with #755 applied, and encoding a known-broken case as a hard failure would turn CI red for reasons unrelated to whatever PR is being reviewed. xfail never fails the suite; when the case starts passing it prints a note to promote it to assert_eq (one-word edit). Measured on macOS bash 3.2.57: main -> 25 passed / 1 xfail, #755 applied -> 26 passed / 0 failed, both deterministic over repeated runs. Note on --jobs 1: workers are still spawned through the same xargs/sh hand-off, so argument propagation is exercised in full. Serializing them keeps a second, unrelated defect out of this case — with two workers running concurrently against one shared --path, the parent exits non-zero on ~3 runs in 5 once the workers actually copy anything (one worker's cp fails with ENOENT on the shared destination). That race is invisible on main only because the workers currently install nothing at all; --jobs 1 or per-tool destinations are clean. Reported in the PR discussion. --- .github/workflows/test-install.yml | 30 +++ CONTRIBUTING.md | 5 + scripts/test-install.sh | 302 +++++++++++++++++++++++++++++ 3 files changed, 337 insertions(+) create mode 100644 .github/workflows/test-install.yml create mode 100755 scripts/test-install.sh diff --git a/.github/workflows/test-install.yml b/.github/workflows/test-install.yml new file mode 100644 index 00000000..4318ac30 --- /dev/null +++ b/.github/workflows/test-install.yml @@ -0,0 +1,30 @@ +name: Test Installer + +# No path filter on purpose: the installer's contract can break from the other +# side too — a renamed division, a file that loses its frontmatter, a change to +# lib.sh — so these run on every PR. +on: + pull_request: + push: + branches: [main] + +jobs: + test-install: + name: install.sh behavior (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # macOS ships bash 3.2, Linux ships bash 5 — the scripts must pass on both. + os: [ubuntu-latest, macos-latest] + steps: + - uses: actions/checkout@v4 + + - name: Shell syntax + run: | + for f in scripts/*.sh; do bash -n "$f"; done + + - name: Run installer tests + run: | + chmod +x scripts/test-install.sh scripts/install.sh + ./scripts/test-install.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fbecb4cf..d136a503 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -240,6 +240,11 @@ Want agency-agents to install into a new tool (a CLI, editor, or agent runtime)? 4. **`.gitignore`** — add a rule for your tool's generated output under `integrations//`. **This step is required and easy to miss.** Converted agent/skill files are generated locally by `convert.sh` and are **never committed** (see "Things we'll always close" below) — only `integrations//README.md` is tracked. Match an existing per-tool entry. 5. **`integrations//README.md`** — a short doc for the integration (every tool has one; it's the only committed file in the tool's directory). 6. **Run `./scripts/check-tools.sh`** — it must pass. It cross-checks `tools.json` against `install.sh` and `convert.sh` and flags anything missing. +7. **Run `./scripts/test-install.sh`** — it must pass. It installs into throwaway + sandboxes (never your real `$HOME`) and pins the installer's observable + contract: where files land, that `--path` beats the tool's env var, that + `--division` / `--agent` / `--agents-file` filter, that `--dry-run` writes + nothing, and that paths with spaces survive. CI runs it on Linux and macOS. If your PR commits the converted output (the generated `integrations//*` files), CI and review will ask you to remove it and add the `.gitignore` rule instead. diff --git a/scripts/test-install.sh b/scripts/test-install.sh new file mode 100755 index 00000000..2be8448e --- /dev/null +++ b/scripts/test-install.sh @@ -0,0 +1,302 @@ +#!/usr/bin/env bash +# +# test-install.sh — regression tests for scripts/install.sh. +# +# install.sh is the largest script in the repo and every install bug so far has +# been a silent one: agents land in the wrong directory, a path with a space is +# split into two, a filter installs everything. These tests pin the observable +# contract — where files land and how many — so those regressions fail loudly. +# +# Design constraints (same as the rest of scripts/): +# * bash 3.2 + BSD userland, no jq, no GNU-only flags. +# * Never touches the real $HOME. Every case runs with HOME set to a fresh +# sandbox, so a broken default path writes into the sandbox, not your config. +# * Only exercises the two source tools (claude-code, copilot) so no case +# depends on convert.sh output being present or fresh. +# +# Usage: ./scripts/test-install.sh [-v] +# -v echo the installer's own output for failing cases + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +INSTALL="$SCRIPT_DIR/install.sh" +# shellcheck source=scripts/lib.sh +. "$SCRIPT_DIR/lib.sh" + +VERBOSE=false +[[ "${1:-}" == "-v" ]] && VERBOSE=true + +passed=0 +failed=0 +xfailed=0 +SANDBOX_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/agency-install-tests.XXXXXX")" +trap 'rm -rf "$SANDBOX_ROOT"' EXIT + +pass() { printf ' ok %s\n' "$1"; passed=$((passed + 1)); } +fail() { + printf ' FAIL %s\n' "$1" + [[ -n "${2:-}" ]] && printf ' %s\n' "$2" + failed=$((failed + 1)) +} + +# assert_eq