Test recipes

Use Testing to choose coverage first. This page holds longer commands for targeted fixtures and mutation testing, not an instruction to run every suite.

Commands start at the repository root. Use the pinned Rust toolchain where Rust is involved. Install only the tools needed for the chosen recipe.

Wrapper and hook tests

Isolated regressions

These use fixture tools rather than bootstrapping or installing the real CLI:

sh .github/workflows/scripts/linux/test-hook-inputs.sh test_staged_ci_guards
sh .github/workflows/scripts/linux/test-hook-inputs.sh test_ci_change_classification
sh .github/workflows/scripts/linux/test-hook-inputs.sh test_staged_ci_guard_deletions
sh .github/workflows/scripts/linux/test-shell-wrapper.sh \
  test_wrapper_preserves_runtime_context test_wrapper_uses_cargo_artifact

With no selector, test-hook-inputs.sh runs all input regressions. Its test_build_version_ref_triggers case compiles the std-only cli/build.rs directly to test Git-ref invalidation of build metadata. It needs the pinned compiler; without it, the case skips.

On Windows, dot-source the wrapper suite to load functions without running the full integration suite:

. .\.github\workflows\scripts\windows\Test-ShellWrapper.ps1
Test-IsolatedWrapperPath
Test-CargoArtifactPath

Full wrapper suites may build/copy binaries or use BINARY_PATH. Read their setup before running all cases.

Checking your real index

These commands inspect staged content and do not create commits:

sh hooks/check-sensitive.sh
sh hooks/check-rust.sh
sh hooks/check-ci-guards.sh
DOTFILES_HOOKS_FULL=1 sh hooks/pre-commit

The Rust and CI guards export the index and reuse Cargo caches without stashing, overwriting, or staging your working files. Helper code and sensitive-pattern rules are read from the checkout. Full mode is still staged-change-aware, not a substitute for committed-range validation. See Hooks.

Full commit-hook suite

Never run test-git-hooks.sh in your developer checkout. It creates real commits in its current repository. Its dirty-tree guard helps prevent accidents, but a clean tree is not a safe fixture; do not use its force override.

This setup creates a fresh scratch repository, copies only the hooks, and keeps the real checkout as the source of the test script:

(
  set -eu
  source_root="$PWD"
  fixture=$(mktemp -d)
  printf 'Hook fixture: %s\n' "$fixture"
  mkdir "$fixture/hooks"
  cp -R hooks/. "$fixture/hooks/"
  cd "$fixture"
  unset GIT_DIR GIT_COMMON_DIR GIT_WORK_TREE GIT_INDEX_FILE
  unset GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES
  unset GIT_CONFIG GIT_CONFIG_PARAMETERS
  export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_COUNT=0
  git init -q --template=
  git config core.hooksPath .git/hooks
  git config user.name "Hook Test"
  git config user.email "ci@test.local"
  git add hooks
  git commit -qm baseline
  cp hooks/pre-commit .git/hooks/pre-commit
  chmod +x .git/hooks/pre-commit
  DIR="$source_root" sh "$source_root/.github/workflows/scripts/linux/test-git-hooks.sh"
)

Inspect and remove only the printed scratch directory afterward. Do not reuse a failed fixture: committed test data can turn a later check into an empty diff. Broader installation/application suites also need a disposable home or runner, not just a separate Git directory.

Cleanup retention and Git templates

python3 -B .github/workflows/scripts/linux/test-maintenance.py

This creates isolated repositories and runs cleanup find commands against temporary directories, without starting systemd services. It checks merge/rebase cleanup, protected branches, worktrees, and file/link/directory retention.

The cleanup services use -mtime +7 for ~/tmp and -mtime +30 for ~/screenshots. Because find counts completed 24-hour periods, removal starts at eight and thirty-one days. Symlinks are removed without following targets. Directories must be empty and old enough; removing their contents can refresh their modification time and delay deletion.

Desktop shell

QML logic/layout tests require Qt 6.8 or newer Quick Test:

sh .github/workflows/scripts/linux/check.sh --strict qml

The runner uses inert Quickshell mocks and does not start live services or load installed desktop configuration. Native rendering and focus still need manual testing in the relevant desktop session.

Python helpers and stock-cache concurrency use isolated fixtures:

python3 -B -m unittest discover \
  -s symlinks/config/quickshell/tests/python -p 'test_*.py'
python3 -B -m unittest discover \
  -s symlinks/config/hypr/scripts/tests -p 'test_*.py'
sh .github/workflows/scripts/linux/test-stocks.sh

They mock desktop actions and market requests. On Windows use python -B; Linux-only cases skip. A successful fixture does not prove live compositor, network, or screen-lock behavior.

Application configuration

These use isolated homes and mock commands:

python3 -B .github/workflows/scripts/linux/test-shell-config.py
python3 -B -m unittest discover -s symlinks/vim/tests -p 'test_*.py'
pwsh -NoProfile -File symlinks/config/powershell/tests/Test-Prompt.ps1

The PowerShell fixture covers first-start command discovery, custom Cargo paths, literal profile-fragment directories, and PATH deduplication on reload. Install each suite’s required native tools; report missing-tool skips.

CI selection fixtures

python3 -B .github/workflows/scripts/linux/test-ci-changes.py

This tests classification, aggregate gating, renames/deletions, unusual paths, invalid ranges, and published-release baselines. It uses temporary Git repositories and a mock gh: fixture commits/tags are local and no releases are published.

To run only the pure classification/gate cases:

python3 -B .github/workflows/scripts/linux/test-ci-changes.py \
  ClassificationTests GateTests

Use the local ci stage as well when workflow contracts or syntax changed. See CI and releases.

Focused mutation testing

cargo-mutants deliberately changes source. Install it separately and run it in a disposable checkout of the committed patch. The following Linux recipe uses origin/main as the comparison base; replace it with the destination’s previous commit when needed.

(
  source_root="$PWD"
  mutation_base=$(git rev-parse --verify 'origin/main^{commit}') || exit 1
  mutation_head=$(git rev-parse --verify 'HEAD^{commit}') || exit 1
  mutation_root=$(mktemp -d) || exit 1
  printf 'Mutation fixture: %s\n' "$mutation_root"
  git clone --quiet --template= --shared --no-checkout \
    "$source_root" "$mutation_root/source" || exit 1
  git -C "$mutation_root/source" -c core.hooksPath=/dev/null \
    checkout --quiet --detach "$mutation_head" || exit 1
  cd "$mutation_root/source/cli" || exit 1
  git diff --relative=cli --unified=0 \
    "$mutation_base" "$mutation_head" -- src > mutants.diff || exit 1
  if [ ! -s mutants.diff ]; then
    echo "No changed Rust source; mutation testing did not run."
    exit 0
  fi
  export XDG_STATE_HOME="$mutation_root/state" DOTFILES_LOG_DIR="$mutation_root/logs"
  cargo mutants --in-place --no-shuffle --in-diff mutants.diff \
    --exclude-re '^src/infra/elevation\.rs:.*\b(run_elevated_child|wait_if_elevated)\b' \
    --exclude-re '^src/domains/dotfiles/resources/path_entry\.rs:.*\b(decode_reg_string|append_user_path|user_path_contains)\b' \
    --exclude-re '^src/domains/ai/apm/cowork\.rs:.*\bis_desired_skill_directory\b' \
    --exclude-re '^src/domains/system/resources/registry\.rs:.*\bnative::' \
    --exclude-re '^src/domains/ai/resources/agent_settings\.rs:[0-9]+:[0-9]+: replace match guard metadata\.is_symlink\(\) with true in <impl Resource for AgentSettingResource>::apply$' \
    --cargo-arg=--profile=ci --cargo-arg=--locked
  mutation_status=$?
  echo "Results: $mutation_root/source/cli/mutants.out"
  exit "$mutation_status"
)

Inspect results before removing the printed temporary directory. To narrow a costly run, add --file src/path/to/module.rs or --re 'function_name' and record that reduced scope.

The exclusions mirror Linux CI: Windows-only elevation, registry, PATH, and Cowork directory-identity code cannot receive native Linux coverage. There is also one equivalent agent-settings mutation that canonicalizes a regular file without changing its write destination. Keep exclusions aligned with ci.yml, not a growing local ignore list.

Linux CI divides the applicable changed-code mutants into eight shards; omit --shard locally to cover the selected set. Investigate every MISSED mutant. It may reveal a missing assertion or equivalent behavior, not necessarily a production bug. Native Windows tests remain necessary, and Windows mutation coverage is not provided.