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). - 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(). - Pipeline-step tests put a tiny non-race helper (
internal/pipeline/fakecli, built once bystepstest.Init) on PATH asgh/glab/git. Never re-link the race-instrumented test binary as those names. - 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.