From 658a66e14b4c5b7629fac3bd2e7f4a7787103c51 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 23 Jun 2026 09:54:05 +0200 Subject: [PATCH 1/6] Restructure integration-test just targets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `just e2e` was the visible-UI runner, but it's only useful for a single test (and even then only with --sandbox/--slow); running it without arguments is far too slow, yet it was easy to invoke by reflex when `just e2e-all` (run all headlessly) was meant. Make `just e2e` the everyday headless runner: no arguments runs the whole suite (what e2e-all did), and a test name runs just that one headlessly via `go test -run` — which we had no target for before. The visible-UI runner moves to `e2e-cli`, pairing with the existing `e2e-tui` (the two main.go subcommands). e2e-all is now redundant and removed. Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 5 +++-- justfile | 23 +++++++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2fb36392e..8a4f924e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,8 +21,9 @@ Windows box has only `just`). - `just format` — `gofumpt -l -w .`. Run before every commit. - `just build` — build the binary. - `just unit-test` — `go test ./... -short`. -- `just e2e-all` — run all integration tests headlessly (`just e2e ` runs a - single one with a visible UI). +- `just e2e` — run all integration tests headlessly; `just e2e ` runs a + single one headlessly too. `just e2e-cli ` runs one with a visible UI + (most useful with `--sandbox` or `--slow`). - `just lint` — run golangci-lint. ## When to commit diff --git a/justfile b/justfile index e7f9fcdc5..c6785b933 100644 --- a/justfile +++ b/justfile @@ -23,7 +23,7 @@ unit-test: # Run both unit tests and integration tests. [unix] -test: unit-test e2e-all +test: unit-test e2e # On Windows, integration tests are not supported right now [windows] @@ -39,18 +39,29 @@ format: lint: ./scripts/golangci-lint-shim.sh run -# Run integration tests with a visible UI. Most useful for running a single test; for running all tests, use `e2e-all` instead. +e2e-test-command := "go test pkg/integration/clients/*.go" + +# Run integration tests headlessly: no args runs all tests, a test name (or path) runs just that one. Use e2e-cli for a visible UI. e2e *args: + {{ if args == "" { e2e-test-command } else { \ + e2e-test-command + " -run 'TestIntegration/" + \ + replace( \ + replace_regex( \ + replace_regex(args, '\S*pkg/integration/tests/', ''), \ + '\.go( |$)', '${1}' \ + ), \ + " ", "$' && " + e2e-test-command + " -run 'TestIntegration/" \ + ) + "$'" \ + } }} + +# Run a single integration test with a visible UI; most useful with --sandbox or --slow. +e2e-cli *args: go run cmd/integration_test/main.go cli {{ args }} # Open the TUI for running integration tests. e2e-tui *args: go run cmd/integration_test/main.go tui {{ args }} -# Run all integration tests headlessly (without a visible UI). -e2e-all: - go test pkg/integration/clients/*.go - # Run some tests on the current commit, similar to what CI does. check: ./scripts/check_commit.sh From 21f13fc3c790084e44f61aefbe406f10c08573d2 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 23 Jun 2026 13:07:39 +0200 Subject: [PATCH 2/6] Add zsh completion for the e2e integration-test recipes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit just's completion is clap-dynamic and exposes no hook for completing a recipe's arguments, so `just e2e ` couldn't suggest anything. Wrap just's completer: for the e2e/e2e-cli recipes, complete the test names found under pkg/integration/tests/, delegating everything else back to just. The names are fed to _multi_parts so they complete one "/"-separated segment at a time — an empty offers just the categories, then drills into the tests within a category — and the .go extension and the shared helper files are stripped so the candidates are exactly the names the recipe accepts. Source it from ~/.zshrc (after compinit) to enable; it's a no-op without just installed and only activates inside a repo with a justfile and a pkg/integration/tests/ directory. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/just_e2e_completion.zsh | 56 +++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 scripts/just_e2e_completion.zsh diff --git a/scripts/just_e2e_completion.zsh b/scripts/just_e2e_completion.zsh new file mode 100644 index 000000000..fa6ab32cd --- /dev/null +++ b/scripts/just_e2e_completion.zsh @@ -0,0 +1,56 @@ +# Zsh completion for the `e2e` and `e2e-cli` recipes in lazygit's justfile. +# +# These recipes take integration-test names (e.g. submodule/reset). This makes +# `just e2e ` complete them from pkg/integration/tests/. To enable it, add +# the following to your ~/.zshrc, *after* the line that runs `compinit`: +# +# source /path/to/lazygit/scripts/just_e2e_completion.zsh +# +# It is a no-op when `just` isn't installed, and only kicks in inside a project +# that has a justfile and a pkg/integration/tests/ directory, so it is harmless +# to source unconditionally. + +(( $+commands[just] )) || return 0 + +# just's own completion is clap-dynamic and has no hook for completing a +# recipe's arguments, so we wrap it: handle the e2e recipes ourselves and +# delegate everything else (recipe names, flags, ...) to just's completer. +source <(JUST_COMPLETE=zsh just) # defines _clap_dynamic_completer_just + +_just_lazygit_e2e() { + if (( CURRENT > 2 )); then + case ${words[2]} in + e2e | e2e-cli) + # Find the justfile's directory, then complete the integration + # tests under pkg/integration/tests/ relative to it. + local dir=$PWD testdir= + while [[ $dir != / ]]; do + if [[ -e $dir/justfile || -e $dir/.justfile || -e $dir/Justfile ]]; then + testdir=$dir/pkg/integration/tests + break + fi + dir=${dir:h} + done + if [[ -d $testdir ]]; then + # A test's name is its path under pkg/integration/tests/ without + # the .go extension, e.g. submodule/reset. Build that list, then + # let _multi_parts complete it one "/"-separated segment at a + # time, so an empty offers only categories. + local -a tests + tests=($testdir/**/*.go(.N:r)) # strip the .go extension + tests=(${tests#$testdir/}) # make relative to the tests dir + tests=(${(M)tests:#*/*}) # keep category/name (drop top-level helpers) + tests=(${tests:#shared/*}) # drop the cross-directory shared package + tests=(${tests:#*/shared}) # drop per-category shared.go helpers + local expl + _wanted tests expl 'integration test' _multi_parts / tests + return + fi + ;; + esac + fi + + _clap_dynamic_completer_just "$@" +} + +compdef _just_lazygit_e2e just # bind last so this wins over the default From e33b6f93970409e589c95ec97cd84fc79f6752a2 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 23 Jun 2026 13:30:24 +0200 Subject: [PATCH 3/6] Document running the integration tests via the just recipes The integration README still described the raw `go run cmd/integration_test` and `go test` invocations, which are easy to get wrong (the headless go-test command in particular) and don't match how we actually run the tests. Rewrite the running/debugging/sandbox instructions around the justfile's e2e recipes instead, and point at the optional zsh completion script. Also switch the test-list regeneration hint to `just generate`, matching the rest of our docs. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/integration/README.md | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/pkg/integration/README.md b/pkg/integration/README.md index 0c50d8f4e..d3b753f06 100644 --- a/pkg/integration/README.md +++ b/pkg/integration/README.md @@ -2,21 +2,21 @@ The pkg/integration package is for integration testing: that is, actually running a real lazygit session and having a robot pretend to be a human user and then making assertions that everything works as expected. -TL;DR: integration tests live in pkg/integration/tests. Run integration tests with: +TL;DR: integration tests live in pkg/integration/tests, and we run them through the [`just`](https://github.com/casey/just) recipes in the repo's `justfile`. Run the whole suite headlessly with: ```sh -go run cmd/integration_test/main.go tui +just e2e ``` -or +or open a terminal UI to browse and run individual tests with: ```sh -go run cmd/integration_test/main.go cli [--slow or --sandbox] [testname or testpath...] +just e2e-tui ``` ## Writing tests -The tests live in pkg/integration/tests. Each test is registered in `pkg/integration/tests/test_list.go` which is an auto-generated file. You can re-generate that file by running `go generate ./...` at the root of the Lazygit repo. +The tests live in pkg/integration/tests. Each test is registered in `pkg/integration/tests/test_list.go` which is an auto-generated file. You can re-generate that file by running `just generate` at the root of the Lazygit repo. Each test has two important steps: the setup step and the run step. @@ -38,19 +38,18 @@ The run step has two arguments passed in: ## Running tests -There are three ways to invoke a test: +We drive the integration tests through the [`just`](https://github.com/casey/just) recipes in the repo's `justfile`, so you'll want `just` installed to run them as described here. (The recipes are thin wrappers, so if you can't install `just`, the underlying commands are right there in the `justfile`.) -1. go run cmd/integration_test/main.go cli [--slow or --sandbox] [testname or testpath...] -2. go run cmd/integration_test/main.go tui -3. go test pkg/integration/clients/*.go +- `just e2e` — run the whole suite headlessly, with no visible UI. This is what CI does, and the fastest way to run everything. +- `just e2e ` — run a single test headlessly, e.g. `just e2e commit/new_branch`; the fastest way to run one test. You can pass several names at once, or a full file path like `pkg/integration/tests/commit/new_branch.go`. +- `just e2e-cli [--slow|--sandbox|--debug] ` — run a single test in a *visible* lazygit UI, so you can watch it (see slow mode below, and sandbox mode and debugging in the following sections). +- `just e2e-tui` — open a terminal UI for browsing and running tests; the easiest way to find and run a test without having to type its name. -The first, the test runner, is for directly running a test from the command line. If you pass no arguments, it runs all tests. -The second, the TUI, is for running tests from a terminal UI where it's easier to find a test and run it without having to copy it's name and paste it into the terminal. This is the easiest approach by far. -The third, the go-test command, intended only for use in CI, to be run along with the other `go test` tests. This runs the tests in headless mode so there's no visual output. +The name of a test is based on its path, so the name of the test at `pkg/integration/tests/commit/new_branch.go` is `commit/new_branch`. -The name of a test is based on its path, so the name of the test at `pkg/integration/tests/commit/new_branch.go` is commit/new_branch. So to run it with our test runner you would run `go run cmd/integration_test/main.go cli commit/new_branch`. +zsh users can get tab-completion of these test names — `just e2e sub` expands to `submodule/…` — by sourcing `scripts/just_e2e_completion.zsh` from their `.zshrc`; see the comment at the top of that file for details. -You can pass the INPUT_DELAY env var to the test runner in order to set a delay in milliseconds between keypresses or mouse clicks, which helps for watching a test at a realistic speed to understand what it's doing. Or you can pass the '--slow' flag which sets a pre-set 'slow' key delay. In the tui you can press 't' to run the test in slow mode. +To watch a test run at a realistic speed, pass `--slow` to `just e2e-cli`; it sets a pre-set delay between keypresses and mouse clicks. For finer control, set the `INPUT_DELAY` env var to a number of milliseconds instead, e.g. `INPUT_DELAY=200 just e2e-cli commit/new_branch`. In the TUI you can press 't' to run a test in slow mode. The resultant repo will be stored in `test/_results`, so if you're not sure what went wrong you can go there and inspect the repo. @@ -67,8 +66,8 @@ The test will run in a VSCode terminal: Debugging an integration test is possible in two ways: -1. Use the -debug option of the integration test runner's "cli" command, e.g. `go run cmd/integration_test/main.go cli -debug tag/reset.go` -2. Select a test in the "tui" runner and hit "d" to debug it. +1. Pass `--debug` to `just e2e-cli`, e.g. `just e2e-cli --debug tag/reset`. +2. Select a test in `just e2e-tui` and hit "d" to debug it. In both cases the test runner will print to the console that it is waiting for a debugger to attach, so now you need to tell your debugger to attach to a running process with the name "test_lazygit". If you are using Visual Studio Code, an easy way to do that is to use the "Attach to integration test runner" debug configuration. The test runner will resume automatically when it detects that a debugger was attached. Don't forget to set a breakpoint in the code that you want to step through, otherwise the test will just finish (i.e. it doesn't stop in the debugger automatically). @@ -76,7 +75,7 @@ In both cases the test runner will print to the console that it is waiting for a Say you want to do a manual test of how lazygit handles merge-conflicts, but you can't be bothered actually finding a way to create merge conflicts in a repo. To make your life easier, you can simply run a merge-conflicts test in sandbox mode, meaning the setup step is run for you, and then instead of the test driving the lazygit session, you're allowed to drive it yourself. -To run a test in sandbox mode you can press 's' on a test in the test TUI or in the test runner pass the --sandbox argument. +To run a test in sandbox mode, press 's' on a test in `just e2e-tui`, or pass `--sandbox` to `just e2e-cli`, e.g. `just e2e-cli --sandbox conflicts/resolve_multiple_files`. ## Tips for writing tests From 23d378ddb9388ce1249dda13ea968b05a788b26a Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 23 Jun 2026 11:03:13 +0200 Subject: [PATCH 4/6] Use `just` instead of `make` in AGENTS.md --- AGENTS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8a4f924e4..4b0ef6731 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,7 +46,7 @@ while still being meaningful and self-contained. - **Every commit must compile and pass all tests.** No "WIP" commits, no commits that leave the tree broken and rely on a follow-up to fix it. -- **Every commit must be `gofumpt`-formatted.** Run `make format` before +- **Every commit must be `gofumpt`-formatted.** Run `just format` before committing. - **Commit messages explain _why_, not _what_.** The diff already shows what changed; the message should capture the motivation, the constraint, or the @@ -284,7 +284,7 @@ So: - For changes to `userConfig` fields specifically, don't edit `docs-master/Config.md` by hand either — the relevant section is auto-generated from the struct field doc comments. After editing the - struct, run `make generate` and include the regenerated + struct, run `just generate` and include the regenerated `docs-master/Config.md` (and `schema-master/config.json`) in your commit. - Don't hard-wrap the doc comments on `userConfig` fields. This applies *only* to `userConfig`, because those comments are fed through the doc From 6064c1091ca125701a7f882462ce8c2d31da2303 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 23 Jun 2026 11:04:40 +0200 Subject: [PATCH 5/6] Remove "Open deprecated test TUI" vscode task I never use this. --- .vscode/tasks.json | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 436275394..ed48672c6 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -61,18 +61,6 @@ "focus": true } }, - { - "label": "Open deprecated test TUI", - "type": "shell", - "command": "go run pkg/integration/deprecated/cmd/tui/main.go", - "problemMatcher": [], - "group": { - "kind": "test", - }, - "presentation": { - "focus": true - } - }, { "label": "Sync tests list", "type": "shell", From 131255ccf013b9eb93aa98c37159ff7baed9c5b2 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 23 Jun 2026 13:15:04 +0200 Subject: [PATCH 6/6] Use headless mode for the "Run current file integration test" vscode task For running an integration test just to see if it fails or succeeds, headless mode is sufficient and actually better, because it works in small terminals like vscode's bottom panel; the "main.go cli" way of running tests tends to fail there because the layout renders differently in such a small window. Headless tests use a fixed window size, so they don't have this problem. It's also slightly faster. The other vscode tasks (slow and sandbox) are unchanged, they only make sense with a visible UI. --- .vscode/tasks.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ed48672c6..dc0ff9673 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -24,7 +24,7 @@ { "label": "Run current file integration test", "type": "shell", - "command": "go run cmd/integration_test/main.go cli ${relativeFile}", + "command": "just e2e ${relativeFile}", "problemMatcher": [], "group": { "kind": "test",