Imported from kunchenguid/no-mistakes (
.agents/skills/testing-conventions/SKILL.md). Install upstream withnpx skills add kunchenguid/no-mistakes --skill testing-conventions. Copyright stays with the author.
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 has three shard kinds so each job's wall stays insidetimeout-minutes: 40and a hang still surfaces asgo test -timeout(15m) rather than an evidence-free job cancel: thewindows-steps-Nrows run./internal/pipeline/steps/...alone (includingsteps/citest), split by test name: each row sets a-run(steps_run) or-skip(steps_skip) regex on the top-level test name, and the last row skips the union of the others so a new test always lands in exactly one row (one shard grew back to the 15m timeout, issue #1580; rebalance the letter ranges from the rows'-vtimings),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, andTestCIWorkflow_WindowsStepsShardsRunEveryTestExactlyOncefails when a declared test or a first-character probe name runs in zero rows or in more than one. 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,TestCIWorkflow_WindowsStepsShardsRunEveryTestExactlyOnce. - 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.
