testing-conventions
SkillDev toolsThis skill documents a Go project's testing conventions. It covers when to prefer e2e tests over unit tests, how the e2e suite is gated behind a build tag, and how to isolate git and environment state in tests using temp dirs and env overrides. It also covers temporary e2e daemons, sharding slow Windows CI tests, and known platform-specific pitfalls.
Then ask your AI: use the testing-conventions skill
Details
Instructions available. Your AI can read the instructions. Execution depends on the setup they require.
Account requirements not reviewed. Check the skill instructions before use; Ahel provides instructions and does not run this skill.
No other account needed.
Have a Go project with an existing test suite and a Makefile.
What your AI can do with it
- Decide between e2e tests and unit tests for a given change
- Keep new e2e tests behind the e2e build tag
- Isolate git state in tests with GIT_CONFIG_* overrides
- Point NM_HOME and HOME at temp dirs in TestMain
- Manage temporary e2e daemons with inventory and reapers
- Shard slow Windows CI tests to stay within timeouts
Getting started
- Have a Go project with an existing test suite and a Makefile.
- Add the skill to your agent's skill directory so it loads when tests change.
- Check that your e2e tests sit behind the e2e build tag and run via make e2e.
- In packages whose tests shell out to git, unset GIT_CONFIG_COUNT and set GIT_CONFIG_NOSYSTEM and GIT_CONFIG_GLOBAL in TestMain.
- In packages that can start a daemon, point NM_HOME and HOME at fresh temp dirs in TestMain.
What this skill tells your AI
The instructions your AI receives, as published by kunchenguid/no-mistakes in .agents/skills/testing-conventions/SKILL.md and read by Ahel’s review.
Testing Conventions
- Prefer e2e tests for behavior that crosses a process or I/O boundary (CLI flags, config loading, git operations, agent spawning, daemon coordination, stdout/stderr, recorded fixtures); unit-test pure helpers where speed and failure localization matter. Prefer creating real git repos in temp dirs over heavy mocking.
- The e2e suite is behind the
e2ebuild tag;make e2erunsscripts/e2e.sh, which sweeps./internal/e2e/...and./internal/pipeline/steps/..., so keep new step-local e2e tests behind the tag too. - Temporary e2e daemons (
NM_TEST_START_DAEMON=1/ harness) are owned byinternal/e2edaemon: exact inventory, concurrency cap (NM_E2E_DAEMON_MAX, default 2), bounded argv checks, and reapers in harness Cleanup, packageTestMain, andscripts/e2e.shEXIT/INT/TERM. A SIGKILL of the wrapper shell does not run its trap; next-run inventory recovery covers that. External sleep-loop keepalives are out of scope. Never point inventory reaping at the shared~/.no-mistakesservice. Regressions:internal/e2edaemon/*_test.go. - Packages whose tests shell out to git unset
GIT_CONFIG_COUNTinTestMainso ambientGIT_CONFIG_*injection from agent harnesses cannot leak in; a test exercising injected config re-sets it witht.Setenv(seeinternal/git,internal/gate,internal/daemon,internal/pipeline/steps,internal/pipeline/steps/citest). - Those
TestMains (plusinternal/evidence,internal/branchsync,internal/gatecontext) also setGIT_CONFIG_NOSYSTEM=1: some distributions shipsafe.bareRepository=explicitin the system git config, which refuses running git inside the bare fixture repositories. Production code passes--git-dirfor bare repositories and must keep working without this. - Every package whose tests run git also points
GIT_CONFIG_GLOBALat a nonexistent path in a temp directory (write no file), so fixture commits never follow the developer'scommit.gpgsign. AHOMEoverride alone does not isolate it, because git also reads$XDG_CONFIG_HOME/git/config. A test that needs a global config writes its own file and re-setsGIT_CONFIG_GLOBALwitht.Setenv(seeTestCIStep_CommitAndPush_GitCommandsUseStandardCredentialEnv). - Packages whose tests can start a daemon or touch ambient state (
cmd/no-mistakes,internal/cli,internal/update) use a package-wideTestMainthat pointsNM_HOMEandHOMEat fresh temp dirs and disables telemetry/update-check env vars, so a full test run never touches a real~/.no-mistakes. Follow the same pattern in new such packages. paths.New()refuses the default~/.no-mistakesroot undergo test; tests that touch app state must setNM_HOMEto a temp dir, and only the production-default path test may opt in withNO_MISTAKES_ALLOW_DEFAULT_ROOT_IN_TESTS=1.- Isolate filesystem and environment state with
t.TempDir()andt.Setenv(). - The Windows CI leg is process-spawn bound, not compute bound: git-backed packages cost roughly 10x their Linux time (
internal/git5.7s -> 53s,internal/branchsync31s -> 415s). The Windows matrix is three shards so each job's wall stays insidetimeout-minutes: 40and a hang still surfaces asgo test -timeout(15m) rather than an evidence-free job cancel:windows-stepsruns./internal/pipeline/steps/...alone (includingsteps/citest),windows-gitruns the remaining git-heavy packages (internal/git,internal/branchsync,internal/gate,internal/evidence,internal/daemon,internal/eval), andwindows-coreis thego listremainder filtered byNM_CI_WINDOWS_GIT_EXCLUDE(the union of the other two shards). Combining steps with the other git-heavy packages madewindows-gita ~21 min floor; the split is the lockstep pin inTestCIWorkflow_WindowsHangSurfacesAsGoTimeoutNotJobCancellation. Keep long git-heavy packages off the serial critical path (internal/branchsyncrunst.Parallel()for exactly that reason) and keep the Defender scan-exclusion step inci.yml, whose comment owns the rationale. GitHub-hosted Windows runners are 4-core; do not assume larger machines. Regressions:TestCIWorkflow_WindowsTestsRunWithScanExclusions,TestCIWorkflow_WindowsHangSurfacesAsGoTimeoutNotJobCancellation. - Go applies an implicit GOOS constraint from a filename suffix, so a test file named
*_windows_test.go(or_linux,_darwin) silently compiles only on that platform. Name platform-agnostic tests about Windows something else. - On macOS with Go 1.26.0-1.26.4, a git-heavy package under
-raceintermittently loses a git child before it runs git: darwinsyscall.rawSyscallwas race-instrumented, andforkAndExecInChildcalls it in the forked child beforeexecve(golang/go#79804, fixed in Go 1.26.5). It surfaces asgit <cmd>: signal: segmentation fault;exit status 66with empty stderr beside a bareThreadSanitizer: CHECK failed: tsan_rtl.cpp:94 "((part)) == ((part1))"line;exec: WaitDelay expired before I/O completefrom a git that exited 0 (a wedged sibling child still holds its inherited pipe); or goroutines parked insyscall.forkExec->readlenuntil the package timeout. Code that folds a git error into a verdict then fails a test (branchsync'sblocked_assumptions_changedwith relationunknown). None of it is a repo bug: checkgo versionand upgrade the toolchain rather than adding retries, waits, or longer timeouts.~/Library/Logs/DiagnosticReports/*.ipsrecords the crash asprocName: <pkg>.test, parentProc: <pkg>.test, asi: "crashed on child side of fork pre-exec"; the CI legs are Linux and Windows. The same bug explains a stray<pkg>.test -test.timeout=...process at high CPU that ignores its own deadline: it is a pre-exec child spinning in__tsan::TraceSwitchPartImplthat inherited the parent's name, argv, and cwd, so no test-side timeout applies to it.internal/procreapreaps those by cwd.
Pipeline step tests (CI latency)
- Fake
gh/glab/giton PATH must be the tiny helper atinternal/pipeline/fakecli, compiled once per test process without-race(stepstest.Init/LinkFakeCLI). Tests that need the underlying Git binary useinternal/testgit.RealGit, which resolves fixed absolute locations without consulting PATH. Do not re-exec the race-instrumented test binary as those names: that was ~0.8-1.1s per spawn and pushedinternal/pipeline/stepsinto the 10-minute package timeout. - CI-monitor tests live in
internal/pipeline/steps/citestso no child ofinternal/pipeline/stepssits near that cap. Both packages run undergo test ./...; do not move them behind thee2etag.
Signals
- GitHub stars
- 9k
- Forks
- 952
- Last commit
- Oct 2026
Questions
- When should I use an e2e test instead of a unit test?
- Prefer e2e tests for behavior that crosses a process or I/O boundary, such as CLI flags, config loading, git operations, agent spawning, daemon coordination, stdout/stderr, and recorded fixtures. Unit-test pure helpers where speed and failure localization matter.
- How do I keep e2e tests out of the normal test run?
- Put them behind the e2e build tag. The make e2e target runs scripts/e2e.sh, which sweeps ./internal/e2e/... and ./internal/pipeline/steps/..., so keep new step-local e2e tests behind the tag too.
Advanced
- Item type
- skill
- Key
testing-conventions-kunchenguid- Source
- github.com/kunchenguid/no-mistakes
github.com/kunchenguid/no-mistakes
Related picks
Skill · davila7
The pick for C / C++golang-code-style
Skill · samber
The pick for Godbos-golang
Skill · sickn33
The pick for Goe2e-testing
Skill · thedaviddias
The pick for End-to-end testinge2e-environment-preflight
Skill · sno-ai
The pick for End-to-end testinggithub-actions-docs
Skill · devantler-tech
The pick for GitHub Actions