Stage 7 · Master
Testing Strategy
Coverage and CI Gates
The first Meridian CI gate measures the shared packages that already exist, and expands module by module as business logic arrives.
Coverage in a go.work Repo
Meridian is not a single-module Go repository. The root go.work file wires together five modules: libs/platform, apps/gateway, apps/identity-service, apps/community-service, and apps/billing-service. That matters for CI. A naive go test ./... from the repository root does not express the desired contract clearly enough, because the gate must be explicit about which modules are covered and how their results roll up into one merge signal.
The current scaffold does not justify a vanity target such as 95% across imagined business logic. The honest first gate is coverage over libs/platform plus the small internal/config packages, then a ratcheting policy as each service grows real handlers, repositories, and worker code.
What Should Be Gated Now
| Package group | Current status | CI expectation |
|---|---|---|
| libs/platform/... | Implemented shared helpers | Required on every pull request |
| apps/*/internal/config | Implemented env mapping | Required on every pull request |
| apps/*/internal/domain | Intentionally empty placeholders | Build only, no artificial coverage target |
| Future repositories / handlers / workers | Not implemented yet | Coverage floor added when the code exists |
This keeps the signal honest. An empty internal/domain/domain.go should compile, but it should not distort the coverage conversation. By contrast, libs/platform/config, logging, errors, response, and tenancy are reusable building blocks with stable behavior, so they deserve a real floor immediately.
A Workspace-Aware Script
#!/usr/bin/env bash
set -euo pipefail
coverage_min="${COVERAGE_MIN:-75}"
packages=(
./libs/platform/...
./apps/gateway/...
./apps/identity-service/...
./apps/community-service/...
./apps/billing-service/...
)
go test -covermode=atomic -coverpkg=./libs/platform/... -coverprofile=coverage.out "${packages[@]}"
total=$(go tool cover -func=coverage.out | awk '/total:/ {gsub("%", "", $3); print $3}')
awk -v total="$total" -v minimum="$coverage_min" 'BEGIN {
if ((total + 0) < (minimum + 0)) {
printf("coverage %.1f%% is below required %.1f%%
", total, minimum)
exit 1
}
}'The important detail is explicit package scope. The script names the workspace modules directly and counts shared-library coverage across those runs with -coverpkg.
The initial floor can stay moderate because the business surface is still small. Once identity-service owns tenants and memberships, or once billing-service adds invoice generation and payment reconciliation, the policy can evolve toward per-module thresholds or a ratchet that prevents coverage from falling in the touched package set.
CI Jobs Beyond Coverage
name: ci
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.work
- name: Run workspace tests
run: make test
- name: Enforce minimum coverage
env:
COVERAGE_MIN: 75
run: ./scripts/check-coverage.sh
vet:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.work
- run: make vetA small scaffold does not need a large CI story. make test, make vet, and a visible coverage floor are enough until richer packages exist.
Coverage is only one gate. vet catches static mistakes; later race-detector jobs matter once Redis-backed caches or billing workers introduce concurrency; Newman smoke tests matter once the gateway exposes more than /healthz. The lesson is sequencing: add gates when the corresponding failure mode becomes possible, not years before.
The Decision
- Coverage is enforced against the code that actually exists today: libs/platform and the per-service config packages.
- The gate is workspace-aware because Meridian is a go.work repository, not a single-module binary.
- Empty placeholder packages compile, but they do not justify inflated coverage requirements.
- Additional CI jobs are added as real failure modes appear: repositories, concurrency, and external HTTP contracts.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.