Skip to content

Testing

Every change keeps the tests green, and every bug fix adds the test that would have caught it.

CommandWhat it runsWhere
just testUnit tests with the race detectorAnywhere
go test ./internal/tui -run TestGolden -updateRewrites the golden screens after an intended change; review the diffAnywhere
just fuzzFuzzes the input filter for 30 s (just fuzz 5m for longer)Anywhere
just lintgo vet, golangci-lint, the name check and the private-detail checkAnywhere
just e2eThe end-to-end suite against a real hostNeeds a test host
just screenshotsThe README images, drawn from demoAnywhere
just reproTwo release builds from empty caches; every binary must matchNeeds GoReleaser and Zig
just test-installinstall.sh against a local, signed snapshot releaseNeeds GoReleaser and Zig

Unit and golden tests

  • Screens are golden files in internal/tui/testdata, at 60×20, 80×24 and 120×36, with motion off. A golden test fails on any change of text or layout. Run it with -update only when the change is meant, and read the diff.
  • Colour is tested separately in internal/tui/theme_test.go: each screen is rendered as a truecolour, 256-colour, 16-colour, NO_COLOR and no-terminal profile would receive it, and every fixed colour must keep a contrast of 3:1 on dark and light backgrounds.
  • Model tests (internal/tui/model_test.go) drive the launcher with keys against a fake backend, and check what it asks the backend to do.
  • The attach path is byte-exact: any change to it needs a test that proves the bytes are unchanged.

End-to-end tests

test/e2e/run.py drives the real client in a pseudo-terminal, as a person in Ghostty would, against a real Linux host.

  1. Use a disposable Linux VM with systemd. The suite starts and deletes sessions named e2e-*, installs the helper, and briefly fills memory to test the cap. Do not point it at a machine you care about.
  2. Make one with simstim host create lima e2e (how to), or add one you reach with ssh: simstim host add <name> <ssh-target>.
  3. Run E2E_HOST=<name> just e2e. It prints PASS or FAIL for each check and a total.
  4. Delete a created host afterwards with simstim host destroy e2e.

A container made with host create docker runs the suite too, over docker exec instead of SSH. It has no systemd, so the memory cap check is skipped. CI runs the suite this way on every change (the docker-host job).

It covers start, attach and detach, resize, byte-exact output, replay after a dropped link, a simulated laptop sleep, the memory cap, snapshots after the ring wraps, a crashed shadow terminal, image paste and file drops, worktrees, ports, rename and delete. Host reboots are tested by hand, with the host owner’s agreement.

test/e2e/agent_setup.py checks setting up hosts and sessions: the agent checks, a secret that reaches the session’s environment and no file, log, command line or unit on the host, setup scripts with and without trust, and host doctor --fix on a stale socket it makes. It uses a throwaway client config, so your own secrets and answers are never read. It installs no agent: it runs agents install only for an agent the host already has, which installs nothing. To run only these checks, set E2E_ONLY=agent-setup.

test/e2e/guided_setup.py checks the guided setup: setup --host <host> --agents shell --yes --no-login connects the host, shows the checklist, installs nothing, asks nothing, and ends with the summary and the first session’s command; setup without a terminal or flags, or with an unknown agent, refuses before it changes anything; and Ctrl+C at the first question stops it and says nothing was changed. It uses a throwaway client config with a copy of hosts.toml. To run only these checks, set E2E_ONLY=guided-setup.

Benchmarks

The attach pump has benchmarks at each step a byte takes: the host’s output ring and screen model (internal/ring, internal/host), the frames (internal/proto), the client’s catch-up and mode tracker (internal/term), the output frames through the client’s attach (internal/client), and the input filter (internal/keys). internal/term/termtest makes the output they use: redraws shaped like a coding agent’s.

just bench (or scripts/bench.sh <base>) builds the benchmarks at a base commit and in the working tree, runs them in turns on this machine, and compares them with benchstat. It fails when a benchmark is more than 10 % slower with p below 0.05. CI runs it on every pull request against the base branch, with a 15 % limit for the noisier runners.

Numbers on an Apple silicon laptop, 15 October 2026, per 32 KiB of agent output:

StepTimeThroughput
Ring write (host)0.4 µs75 GB/s
Screen model (host, for status rules)1.3 ms26 MB/s
Output frame write (host)12 µs2.8 GB/s
Output frame through the client’s attach63 µs520 MB/s

The screen model is the slowest step. It is a Go terminal emulator used only to read the screen for status rules, and 26 MB/s is far more than an agent prints.

Vulnerabilities and dead code

just vulncheck runs govulncheck, which reports known vulnerabilities only in code the project calls, and deadcode, which fails on any function that neither the program nor a test reaches. CI runs both on every push.

Time to first agent

scripts/time-to-agent.sh <ssh target> [ssh config] [agent] measures the path in getting started on a host you can already reach: it builds a snapshot release, signs it with a throwaway key, serves it on localhost, and times install.sh, host add and new --agent in a throwaway config folder, until the agent’s state is known.

DateClientHostinstallhost addagent runningTotal
2026-10-15macOS, Apple siliconDebian 13 VM, over a jump host on WiFi0.1 s1.8 s0.2 s (Claude Code)2.2 s

The release is served locally, so the download is not in the time: a client archive is about 11 MB, a few seconds on a home connection. The rest of the five minutes is typing three commands, and the agent’s own login.

Seeing a screen

test/ttyshot runs a program in a pseudo-terminal, sends keys from a script, and prints the screen it ends on, or draws it as SVG:

go run ./test/ttyshot -script keys.tty -- ./bin/<name> demo

just screenshots uses it to draw the README’s images.