Skip to content
gwmgwmgwmv1.10.0

Testing

cargo test runs the full suite: ~1900 tests (1902 #[test] markers as of v1.0.0) across 75 integration files under tests/ plus the unit tests embedded in src/. Average run on a recent laptop: ~1 second.

The suite runs on every push as a matrix across ubuntu-latest, macos-latest, and windows-latest (wired in ci.yml); each merge into dev is gated on all three turning green, and pre-release artifacts are built from the same commit by pre-release.yml. Windows was added to the matrix in v0.8.0-rc.1 (#112).

No production code lands without a failing test that pinned the behaviour down first. This is a hard merge requirement, not a guideline (see CLAUDE.md). The loop is red → green → refactor:

  1. Red: write a failing test that captures the new behaviour or the bug. Run it; it MUST fail for the right reason (an assertion mismatch, not an unrelated compile error).
  2. Green: write the minimum production code to make it pass. No speculative branches or abstractions.
  3. Refactor: clean up while the tests are green, re-running the full suite after each step.

Anything observable from outside the function under test counts as behaviour: a new CLI subcommand / flag / output format → end-to-end test in tests/cli_binary.rs (and a companion file per surface, e.g. tests/exec_tests.rs, tests/clean_tests.rs, tests/review_tests.rs, tests/statusline_tests.rs, tests/daemon_tests.rs); a new public function → unit test in tests/<module>_tests.rs; a new bootstrap step → integration test in tests/bootstrap_tests.rs; a libgit2 worktree operation → tests/worktree_integration.rs; a TUI state transition → tests/tui_app_tests.rs (or a focused tests/tui_state_*_tests.rs slice). “I tested it manually” is not an exception: codify the manual test.

Terminal window
cargo test # full suite (~1900 tests)
cargo test --test tui_app_tests # one integration file
cargo test refresh_github # name-substring filter across the whole suite
cargo test -- --nocapture # let println! / dbg! through
cargo test --release # opt-level=3, same suite

The --test <name> filter is by integration file, not by module: cargo test --test tui_app_tests runs everything inside tests/tui_app_tests.rs.

Since v0.8.0-rc.5 a user-level global config at ~/.config/gwm/config.toml is merged underneath each repo’s .gwm.toml. To keep cargo test hermetic the test suite injects None for the global layer (the *_layered seams), so a run never reads the contributor’s real global config. Set GWM_NO_GLOBAL_CONFIG=1 to force strictly repo-only loading at runtime too (#194 / #196).

All integration tests live under tests/:

tests/
├── common/ # shared helpers (init_repo, paths_equal)
│
│ # CLI surface
├── cli_binary.rs # end-to-end assert_cmd coverage of the CLI surface
├── cli_format_tests.rs # remove / prune / sync plan formatters (alignment, detached HEAD)
├── cli_repo_context_tests.rs # repo-context resolution in/out of a git repo (strict vs lenient)
├── exec_tests.rs # gwm exec rollup arithmetic + ✓/✗ aggregation (#313)
├── clean_tests.rs # gwm clean artefact discovery + human-size formatting (#313)
├── review_tests.rs # gwm review branch/dir naming contract (#308)
├── review_integration.rs # gwm review fetch refs/pull/<N>/head + worktree attach (#308)
├── statusline_tests.rs # gwm statusline one-line render (blank / count / dirty / issue·PR) (#309)
│
│ # daemon + JSON API (#38)
├── daemon_tests.rs # pure RPC core: handle_line / dispatch (cross-platform)
├── daemon_integration.rs # socket round-trip (unix + daemon feature only)
├── json_api_tests.rs # --format=json status/projection for list / doctor / path
│
│ # config + bootstrap + hooks
├── config_tests.rs # .gwm.toml parsing + write_default + defaults
├── config_cli_tests.rs # gwm config get/set/unset/list/validate/path/edit
├── config_global_tests.rs # user-level global config merge (overlay semantics)
├── config_set_at_tests.rs # set_value_at typed write-back per layer + round-trip
├── presets_tests.rs # gwm init --preset lookup + alias resolution (#37)
├── bootstrap_tests.rs # copy / guard / no-symlink / [hooks.*] lifecycle phases
├── bootstrap_when_tests.rs # `when:` predicate grammar (file/cmd/env/glob + boolean ops)
├── lifecycle_tests.rs # lifecycle hook execution + command-log recording
├── trust_tests.rs # TOFU trust ledger hashing + (origin, sha256) entries (#95)
│
│ # GitHub workflow
├── github_tests.rs # issue/PR linking + trim_git_suffix + URL derivation
├── gitmoji_tests.rs # gwm commit-prefix (shortcode / unicode / --branch / override)
├── labels_tests.rs # [[labels]] deterministic colour + diff arithmetic
├── milestones_tests.rs # [[milestones]] due-date normalisation + diff arithmetic
├── pr_templates_tests.rs # [pr_template] body rendering + placeholders
├── templating_tests.rs # placeholder substitution + issue-form markdown rendering
│
│ # worktree ops + history
├── worktree_integration.rs # git2 add / list / remove / prune
├── sync_tests.rs # gwm sync fetch + rebase/merge + dirty-tree / no-upstream refusal
├── history_tests.rs # operation journal IO + gwm history + gwm undo contract
├── command_log_tests.rs # command-log append ordering + snapshot clone
├── naming_tests.rs # kebab normalisation + branch validation + parse roundtrip
├── aliases_tests.rs # [aliases] resolution + shadowing + shell-pipeline refusal
├── doctor_tests.rs # gwm doctor checks + severity arithmetic
├── hooks_tests.rs # gwm hooks install commit-msg (force, linked worktree, hooksPath)
├── multiplexer_tests.rs # gwm tmux / zellij / herdr argv construction + env guard
├── launcher_tests.rs # [git_tui] / [review] placeholder expansion + base resolution
├── error_tests.rs # GwmError variants + Display + From impls
├── error_variants_tests.rs # newer GwmError variants (unborn HEAD, gh JSON parse, …)
│
│ # workspace mode (#36)
├── workspace_tests.rs # discover child repos + merge_worktrees
├── cli_workspace_tests.rs # gwm --workspace list / create --repo CLI plumbing
├── tui_workspace_tests.rs # REPO column + swap-on-navigation in the workspace TUI
│
│ # TUI state + render
├── tui_app_tests.rs # App state transitions (ratatui-free)
├── tui_chord_tests.rs # multi-key chord dispatch in list view
├── keymap_tests.rs # [tui.keys] parsing + chord conflicts + prefix collisions
├── modal_keymap_tests.rs # [tui.keys.modal.<context>] single-key bindings (#219)
├── palette_tests.rs # command palette action registry + fuzzy filter
├── theme_tests.rs # [theme] preset resolution + role overrides + colour parsing
├── tui_footer_tests.rs # single-line statusline chips + flush-right log
├── tui_header_tests.rs # single-row header hierarchy + width-driven truncation
├── tui_wt_tree_tests.rs # Working Tree file-tree model (collapse / cap / glyphs) (#300)
├── tui_ui_helpers_tests.rs # ellipsize / layout helpers
├── tui_modal_render_tests.rs # modal overlay rendering against a TestBackend
├── tui_sidebar_render_tests.rs # sidebar panes rendering (commits / status)
├── tui_theme_audit_tests.rs # theme role coverage audit across draw sites
├── tui_theme_integration_tests.rs # resolved theme threaded through draw_* sites
├── tui_state_*_tests.rs # per-slice state (create_form, filter, confirm, sidebar,
│ # spinner, link_prompt, palette, command_logs,
│ # config_panel, async_task, github_fetch,
│ # pty_overlay (#35), …)
│
│ # packaging / release
├── binstall_metadata_tests.rs # [package.metadata.binstall] artefact naming
├── homebrew_formula_tests.rs # Homebrew formula template substitution
├── flake_tests.rs # Nix flake structure (build, devShell, app, overlay)
├── release_workflow_tests.rs # release.yml / pre-release.yml tag + publish-token contract
├── precommit_hook_tests.rs # pre-commit hook scaffolding
└── commit_graph_perf_tests.rs # commit-graph pipe allocation invariants

(Not exhaustive; ls tests/*.rs is the live list (75 files as of v1.0.0); the matrix runs all of them.)

Tests pinned to catch a specific regression are prefixed with a // regression: <one-line> comment inside the test body, so the original incident is discoverable without git blame:

#[test]
fn trim_git_suffix_handles_trailing_slash() {
// regression: PR #68 Copilot review — owner/repo.git/ left ".git" in the slug
assert_eq!(trim_git_suffix("https://github.com/owner/repo.git/"), "owner/repo");
}

Find them all:

Terminal window
grep -rn "regression:" tests/

Suite hygiene (sentinel coverage vs incident catalogue) was last audited in claudedocs/test-audit-0.4.0.md. Re-run the audit after each minor: drift accumulates fast around the launcher and GitHub-linking surfaces.

In addition to the integration files, src/ carries unit tests inside #[cfg(test)] mod tests { … } blocks colocated with the code under test. They cover:

  • Pure functions (kebab normalisation, slug parsing, sigil arithmetic)
  • Serde round-trips on every Config sub-struct
  • Internal predicates not exposed on the public surface

cargo test runs them alongside the integration suite, so they don’t need a separate invocation.

The minimum bar enforced by the pre-commit hook (and by CI):

Terminal window
cargo fmt --check
cargo clippy --all-targets -- -D warnings
cargo test

All three must be green. The pre-commit hook lives in tools/git-hooks/pre-commit; enable it with:

Terminal window
git config core.hooksPath tools/git-hooks

(or via the bundled bootstrap if your repo’s .gwm.toml wires it in).

The Nix flake exports a pinned dev shell (Rust toolchain pinned to the same rust-toolchain.toml version used in CI, plus rust-analyzer, clippy, rustfmt, cargo-watch, cargo-edit, and the libgit2 build deps) without touching the host system:

Terminal window
nix develop # drops you in the shell
cargo test # everything works out of the box

See Integrations → Homebrew and Nix for the full flake surface.

  • Contributing: conventions for adding tests and the regression-comment format
  • Roadmap: upcoming areas that will need new test coverage