The UI coverage gate was tightened to 0.1pp against a fast-local measurement (39.86% baseline); CI's slower runners measure ~0.9pp lower, so tests-ui-e2e failed there. UI e2e coverage is diffusely non-deterministic and tracks machine speed — a 0.1pp band can't hold across environments. Rather than loosen the gate, raise the floor under it: a render-smoke spec mounts each lazy page (navigate + assert the header renders), covering a dozen previously-untested pages and lifting coverage from ~39% to ~42.7% locally. Restore the tolerance to 0.8pp and set the baseline conservatively (40.0), below the slow-CI floor, so the ratchet holds without flapping. Document the coverage policy — install the git hooks and don't bypass them (no --no-verify, no hand-lowering the baseline or widening the tolerance); raise coverage by adding tests instead; set the UI baseline below the slow-CI floor — in AGENTS.md, CONTRIBUTING.md and .agents/building-and-testing.md. Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Richard Palethorpe <io@richiejp.com>
10 KiB
Build and Testing
Building and testing the project depends on the components involved and the platform where development is taking place. Due to the amount of context required it's usually best not to try building or testing the project unless the user requests it. If you must build the project then inspect the Makefile in the project root and the Makefiles of any backends that are effected by changes you are making. In addition the workflows in .github/workflows can be used as a reference when it is unclear how to build or test a component. The primary Makefile contains targets for building inside or outside Docker, if the user has not previously specified a preference then ask which they would like to use.
Building a specified backend
Let's say the user wants to build a particular backend for a given platform. For example let's say they want to build coqui for ROCM/hipblas
- The Makefile has targets like
docker-build-coquicreated withgenerate-docker-build-targetat the time of writing. Recently added backends may require a new target. - At a minimum we need to set the BUILD_TYPE, BASE_IMAGE build-args
- Use
.github/backend-matrix.ymlas a reference — it's the data-only YAML that lists every backend variant'sbuild-type,base-image,platforms, etc. (backend.ymlandbackend_pr.ymlconsume it viascripts/changed-backends.js). - l4t and cublas also require the CUDA major and minor version.
- For llama-cpp / ik-llama-cpp / turboquant the matrix also sets
builder-base-imagepointing at a prebuiltquay.io/go-skynet/ci-cache:base-grpc-*tag. Localmake backends/<name>defaults toBUILDER_TARGET=builder-fromsourceand doesn't need it — the Dockerfile's from-source stage installs everything itself.
- Use
- You can pretty print a command like
DOCKER_MAKEFLAGS=-j$(nproc --ignore=1) BUILD_TYPE=hipblas BASE_IMAGE=rocm/dev-ubuntu-24.04:7.2.1 make docker-build-coqui - Unless the user specifies that they want you to run the command, then just print it because not all agent frontends handle long running jobs well and the output may overflow your context
- The user may say they want to build AMD or ROCM instead of hipblas, or Intel instead of SYCL or NVIDIA insted of l4t or cublas. Ask for confirmation if there is ambiguity.
- Sometimes the user may need extra parameters to be added to
docker build(e.g.--platformfor cross-platform builds or--progressto view the full logs), in which case you can generate thedocker buildcommand directly.
Test coverage gate
The core Go suites (./pkg, ./core, plus the in-process integration suite ./tests/e2e) are covered by a strict, monotonic coverage ratchet:
make test-coverage— runs the suites withcovermode=atomicinstrumentation and writes a merged profile tocoverage/coverage.out. Uses the same prerequisites asmake test.--coverpkg(COVERAGE_COVERPKG = core/...,pkg/...): coverage is attributed to the core+pkg packages, not just the package under test. This is what lets the in-processtests/e2esuite (which drives the real HTTP server over loopback viaapplication.New) credit thecore/http/endpoints/...handlers it exercises — folding it in roughly doubled endpoint coverage (e.g.endpoints/openai13.6% → 52%). The denominator is therefore all ofcore+pkg(minus generated proto, dropped viaCOVERAGE_EXCLUDE_RE), so the number isn't comparable to a plain per-package figure.- Integration suites (
COVERAGE_E2E_ROOTS = ./tests/e2e) run non-recursively (excludestests/e2e/distributed, which needs containers) with--label-filter=!real-models(those need a downloaded model) against the mock backend built byprepare-test.tests/integrationis deliberately excluded — it needsmake backends/local-store, which the coverage CI job doesn't build. - Flake note: folding integration tests into a strict gate means a hard e2e failure (or a spec that silently stops running) can fail the coverage gate, not just the test.
--flake-attemptsabsorbs transient retryable failures; covermode=atomic keeps line coverage deterministic otherwise. - Why one ginkgo run per root (
scripts/run-coverage.sh): passing several recursive roots to a single ginkgo invocation (e.g.ginkgo -r ./pkg ./core) only merges one root's coverprofile into--output-dir/--coverprofile— the others are silently dropped. Verified with ginkgo 2.29.0:-r ./pkg ./coreyields only./pkgcoverage, while-r ./corealone yields all 34 core packages. So the script runs each root separately and concatenates the (disjoint) profiles. Don't "simplify" it back to a single multi-root invocation — that's howcore/(including all ofcore/http, ~7.4k statements) silently vanished from the number before. - Build tags (
COVERAGE_TAGS, passed viaGINKGO_TAGS): defaults todebug auth. Theauthtag is required to compile the real (sqlite-backed) auth implementation and its ~150//go:build authtests — without it those files aren't built, the tests don't run, and the gate scores auth against a stub (~3.7% instead of ~38%). If you add new tag-gated tests, extendCOVERAGE_TAGSor they won't count (and likely won't run in CI at all).
make test-coverage-check— runstest-coverage, thenscripts/coverage-check.shfails the build if total coverage is below the committed baseline incoverage-baseline.txt. The Linux job in.github/workflows/test.ymlruns this instead ofmake test.make test-coverage-baseline— regenerates and overwritescoverage-baseline.txtfrom the current run.make install-hooks— setscore.hooksPathto the versioned.githooks/, whosepre-commitruns checks scoped to what's staged: Go changes →make lint+make test-coverage-check;core/http/react-ui/changes →make test-ui-coverage-check(Playwright e2e + UI coverage gate). A commit touching neither is skipped; bypass withgit commit --no-verify. The hook resolves golangci-lint's new-from base toupstream/master→origin/master→master, so it works from a fork clone whereorigin/masteris stale (passed tomake lintviaLINT_NEW_FROM).
React UI coverage
The React UI (core/http/react-ui/) has no component/unit tests — its only tests are the Playwright e2e specs in e2e/, which run against the real app served by tests/e2e-ui/ui-test-server (the dist is //go:embeded, so the server is rebuilt per coverage run). Those specs do genuinely exercise the UI (clicks, fill, setInputFiles, getByRole/getByText, visibility/value assertions).
make test-ui-coverage— builds an istanbul-instrumented bundle (COVERAGE=true, viavite-plugin-istanbulwithforceBuildInstrument: true— the plugin skips production builds otherwise), re-embeds it intoui-test-server(the dist is//go:embeded), runs the Playwright specs, and writes annycreport tocore/http/react-ui/coverage/. The specs import{ test, expect }frome2e/coverage-fixtures.js(re-exports Playwright's, plus harvestswindow.__coverage__into.nyc_output/after each test). Instrumentation is off unlessCOVERAGE=true, so dev/prod builds and plainmake test-ui-e2eare unaffected (the fixture no-ops whenwindow.__coverage__is absent).- Browser: the flake dev shell ships
chromiumand exportsPLAYWRIGHT_CHROMIUM_PATH;playwright.config.jsuses it vialaunchOptions.executablePath, and the Makefile skipsplaywright installwhen it's set. This avoids Playwright's downloaded browser, which can't resolve system libs (libglib-2.0, …) on NixOS. In CI (noPLAYWRIGHT_CHROMIUM_PATH) the Makefile falls back toplaywright install --with-deps chromium. - The app is a React SPA, so coverage accumulates across in-app navigation within a test; a full
page.goto/reload resets it. .nycrc.jsonusesall: true, so everysrc/**file is in the report, including 0%-coverage ones — that's how you spot features with no test at all (sort the HTML report orcoverage-summary.jsonby line% ascending).- UI coverage gate:
make test-ui-coverage-checkruns the suite thenscripts/ui-coverage-check.sh, failing if total line coverage drops more thanUI_COVERAGE_TOLERANCEbelowcore/http/react-ui/coverage-baseline.txt.make test-ui-coverage-baselineregenerates the baseline. Runs in CI (tests-ui-e2e.yml) and pre-commit oncore/http/react-ui/changes. - Why it has a tolerance (unlike the strict Go gate): UI e2e coverage is non-deterministic. Specs that assert on state and end while async/lazy render work is still in flight collect those lines only when the render beats the coverage teardown — so the total drifts with machine speed/load (a fast local box reads higher than a slow CI runner), diffusely across many specs. The tolerance absorbs that drift, so set the baseline below the slow-CI floor, never to a fast-local
make test-ui-coverage-baselinenumber, or CI flaps. - Raising coverage is cheap: a render-smoke spec (navigate to a route, assert its header renders) mounts a lazy page and runs its full render + initial effects, capturing most of its lines in a few lines of test — see
e2e/page-render-smoke.spec.js. Auth is disabled in the test server (isAdmin=true), soRequireAdmin/RequireFeatureroutes render without a mock. The most deterministic win is removing a race: make a specawaita rendered element before ending (seee2e/agents.spec.js→ AgentCreate) so its lines count every run.
Rules (both gates):
- Install the hooks:
make install-hooksonce per clone so lint + coverage run pre-commit. Don't lean on CI for what the hook catches. - Don't work around the gate: never
git commit --no-verify, and never hand-lower a baseline or widen a tolerance to turn a red gate green. The ratchet only moves up. - If a change drops coverage, add tests (sort
coverage-summary.jsonby line% ascending to find untested code) rather than editing the baseline. When coverage legitimately rises, commit the regenerated baseline (make test-coverage-baseline/test-ui-coverage-baseline). - The Go gate is strict — no tolerance;
covermode=atomickeeps it deterministic. The UI gate keeps a small tolerance only because its e2e coverage isn't.