From b1d8b86186d1649ff19b777762383ba2959920b0 Mon Sep 17 00:00:00 2001 From: Benjamin Smidt Date: Fri, 15 May 2026 09:56:50 -0700 Subject: [PATCH 1/6] docs(plans): activate plan for covgate --exclude flag Co-Authored-By: Claude Opus 4.7 --- plans/active/20260515-covgate-exclude-flag.md | 327 ++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 plans/active/20260515-covgate-exclude-flag.md diff --git a/plans/active/20260515-covgate-exclude-flag.md b/plans/active/20260515-covgate-exclude-flag.md new file mode 100644 index 0000000..70dd22a --- /dev/null +++ b/plans/active/20260515-covgate-exclude-flag.md @@ -0,0 +1,327 @@ +# Add `--exclude` flag to covgate for skipping selected Go packages + +This ExecPlan is a living document. The sections Progress, Surprises & Discoveries, Decision Log, and Outcomes & Retrospective must be kept up to date as work proceeds. + +## Scope + +| Repository | Access | Description | +|-----------|--------|-------------| +| `gotools/` | read-write | Add `Exclude` option to covgate service, wire `--exclude` CLI flag, add tests | +| `backend/` | read-only | Downstream consumer that motivates the flag (no edits here) | + +This plan lives in `gotools/plans/backlog/` because all code changes happen in `internal/services/covgate/` and `internal/commands/` inside the `gotools` repo. + +## Purpose / Big Picture + +`miru covgate` enforces per-package Go coverage thresholds. Today every package matched by `--packages` is measured, even when some of those packages cannot run their tests in the current environment (e.g., packages that talk to AWS, Cloudflare, or other cloud services during tests). Downstream consumers (notably `backend/`) need a way to skip those packages locally while CI still measures everything. + +After this change, a user invoking `miru covgate` may pass a `--exclude` flag whose value is a comma-separated list of Go list patterns. Those patterns are resolved through `go list` and the resulting import paths are removed from the measurement set before tests run. Existing invocations (no `--exclude` flag, or `--exclude=""`) behave identically to today. + +Observable behavior after the change, run from a module whose `./...` enumerates `pkg/a`, `pkg/b`, `pkg/c`: + + $ go tool miru covgate --packages ./... --exclude ./pkg/b + Checking per-package coverage (default minimum: 80.0%)... + + Excluded 1 package from coverage measurement + STATUS COVERAGE REQUIRED TIME PACKAGE + ------ -------- -------- -------- ------- + PASS 90.0% 80.0% 0.5s pkg/a + PASS 90.0% 80.0% 0.5s pkg/c + + Total time: 1.0s + All packages meet minimum coverage requirement + +And the no-exclude form is byte-for-byte unchanged from today: + + $ go tool miru covgate --packages ./... + Checking per-package coverage (default minimum: 80.0%)... + + STATUS COVERAGE REQUIRED TIME PACKAGE + ... + +## Progress + +- [ ] Milestone 1 — Add `Exclude` field to `Opts`, implement set-subtract logic in `(*runner).run`, including the "Excluded N package(s) from coverage measurement" notice +- [ ] Milestone 2 — Wire `--exclude` flag in `internal/commands/covgate.go` and update `internal/commands/commands_test.go` +- [ ] Milestone 3 — Add table-driven tests in `internal/services/covgate/covgate_test.go` covering the five exclusion scenarios +- [ ] Milestone 4 — Run preflight (lint + covgate + surface lint) and ensure it reports clean; fix anything flagged + +Use timestamps when you complete steps. Split partially completed work into "done" and "remaining" as needed. + +## Surprises & Discoveries + +(Add entries as work proceeds.) + +## Decision Log + +- Decision: Resolve exclusion patterns by re-invoking `goListPackages` for each comma-separated entry rather than literal string matching against import paths. + Rationale: The flag's value should accept the same dialect as `--packages` (e.g., `./internal/pkg/aws/...`), so it must go through `go list` to expand wildcards correctly. Treating the flag's elements as raw import paths would silently fail for any `/...` pattern. + Date/Author: 2026-05-15, planning subagent. + +- Decision: Print the "Excluded N package(s)" notice only when exclusion actually removed packages, and only after the header line but before `printHeader`. + Rationale: A no-op exclusion (pattern matched nothing) should not pollute output. Placing the notice before the table header keeps it adjacent to the existing intro line and away from per-package rows. + Date/Author: 2026-05-15, planning subagent. + +- Decision: Empty/unset `Exclude` preserves byte-identical existing output. + Rationale: Required to avoid regressing every existing caller and CI invocation. + Date/Author: 2026-05-15, planning subagent. + +- Decision: Trim whitespace around each comma-separated entry, and ignore empty entries after trimming. + Rationale: Tolerates `"a, b, c"`-style input without surprising failures, matching common CLI conventions. + Date/Author: 2026-05-15, planning subagent. + +## Outcomes & Retrospective + +(Summarize at completion or major milestones.) + +## Context and Orientation + +Covgate is a Go test-coverage gate exposed as the `covgate` subcommand of the `miru` CLI (built from `cmd/miru/main.go`). The reader should treat the following files as authoritative starting points; all paths are relative to the repo root `gotools/`. + +Service layer: + +- `internal/services/covgate/covgate.go` — service logic. + - `Opts` struct currently has fields `Packages`, `SrcPrefix`, `TestDir`, `DefaultThreshold`, `Parallelism`, `TightnessEnabled`, `TightnessTolerance`, `Out`. + - `runner` struct injects three functions for testability: `goModule`, `goListPackages`, `measure`. Tests substitute their own implementations. + - `Run(opts Opts) error` wires in the real `gocover.GoListPackages` and `gocover.GoModule`, then calls `(*runner).run`. + - `(*runner).run` calls `r.goListPackages(opts.Packages)` to get the package list, prints `printHeader`, then `runPackages` → `printResults`. +- `internal/services/gocover/gocover.go` — `GoListPackages(pattern string) ([]string, error)` runs `go list ` and returns trimmed non-empty lines. + +CLI layer: + +- `internal/commands/covgate.go` — `NewCovgateCommand()` returns the cobra command. It already binds each `Opts` field to a `--` flag using `cmd.Flags()`. Default `Out` is `os.Stdout`. +- `internal/commands/root.go` — registers `NewCovgateCommand()` under the `miru` root via `cmd.AddCommand`. + +Tests: + +- `internal/services/covgate/covgate_test.go` — uses a `runner{}` literal with injected `goModule`, `goListPackages`, `measure`. Existing tests like `TestRun_AllPass` and `TestRun_WithFailure` show the established pattern. +- `internal/commands/commands_test.go` — `TestNewCovgateCommand_Flags` and `TestNewCovgateCommand_FlagDefaults` assert each flag's existence, type, and default value. They must be updated for the new `--exclude` flag. + +Validation scripts (all under `scripts/`): + +- `scripts/preflight.sh` — runs `lint.sh`, `covgate.sh`, and `lint-surface.sh` in parallel; exits non-zero if any fails. **This is the canonical "preflight" command.** A clean preflight is mandatory before publishing changes. +- `scripts/lint.sh` — Go lint (gofumpt + golangci-lint). +- `scripts/covgate.sh` — runs `go tool miru covgate` against this repo's own packages. +- `scripts/lint-surface.sh` — exported-API stability check. + +Glossary: + +- **Go list pattern**: the string form `go list` accepts on its command line, e.g., `./...`, `./pkg/...`, or `example.com/mod/internal/foo`. Wildcards expand to one import path per matched package. +- **Set-subtract**: build a set `E` of import paths from exclude patterns; rebuild the main list keeping order, dropping any path that appears in `E`. +- **Preflight clean**: `./scripts/preflight.sh` exits 0 and prints `=== All checks passed ===`. + +## Plan of Work + +### Milestone 1 — Service-layer changes + +Edit `internal/services/covgate/covgate.go`: + +1. Add a new field to `Opts`: + + Exclude string + + Place it immediately after `Packages` so related fields stay together. Document semantics in a comment: comma-separated Go list patterns; empty means no exclusion. + +2. In `(*runner).run`, after the existing `pkgs, err := r.goListPackages(opts.Packages)` block but before `printHeader(w)`, insert logic that: + - Returns early (no change) when `strings.TrimSpace(opts.Exclude) == ""`. + - Splits `opts.Exclude` on commas, trims whitespace from each entry, drops empty entries. + - For each non-empty entry, calls `r.goListPackages(entry)` to resolve the pattern, collecting all returned import paths into a `map[string]struct{}` named `excluded`. + - If any `r.goListPackages` call returns an error, propagate it with `fmt.Errorf("resolve exclude %q: %w", entry, err)`. + - Rebuilds `pkgs` by iterating the original slice and keeping only entries whose import path is not in `excluded` (preserves order, deduplicates trivially because the original list contains unique paths). + - If the rebuilt length is smaller than the original, print one line to `w`: + + Excluded N package(s) from coverage measurement + + using `fmt.Fprintf` and the actual count. Only print when the count is `> 0` — a pattern matching nothing produces no notice. + +3. The rebuilt `pkgs` flows into the existing `printHeader(w)` and `runPackages(...)` paths unchanged. When `pkgs` is empty after exclusion, the existing code in `runPackages` (a zero-length loop) and `printResults` (no failures) already produces a clean "All packages meet minimum coverage requirement" outcome with `Total time: 0.0s` — verify by test rather than adding new branches. + +### Milestone 2 — CLI wiring + +Edit `internal/commands/covgate.go`: + +1. Inside `NewCovgateCommand`, after the existing `fl.StringVar(&opts.Packages, ...)` line, add: + + fl.StringVar( + &opts.Exclude, "exclude", "", + "comma-separated list of Go list patterns to exclude from coverage measurement", + ) + +2. No other CLI changes are needed; cobra already binds the value into `opts.Exclude` and passes it through to `covgate.Run` via the existing `RunE` closure. + +Edit `internal/commands/commands_test.go`: + +1. In `TestNewCovgateCommand_Flags`, extend the `tests` table with `{"exclude", "string"}`. +2. In `TestNewCovgateCommand_FlagDefaults`, extend `stringDefaults` with `"exclude": ""`. + +### Milestone 3 — Service tests + +Edit `internal/services/covgate/covgate_test.go`. Add a new `TestRun_Exclude_*` family modeled on `TestRun_AllPass`. The test file already imports `bytes`, `strings`, and `testing`; the new tests need no additional imports. + +Use a single table-driven helper plus per-case `t.Run` subtests. For each subtest: + +- Provide a stub `goListPackages` that returns a fixed list when called with the `Packages` arg and a different (or empty) list when called with each exclude pattern. Easiest pattern: a `map[string][]string` keyed on the pattern, with a closure that looks it up. +- Inject `goModule` returning `modName`. +- Inject `measure` with `fakeMeasure(90.0)` (always pass). +- Pass `Opts{Out: &buf, DefaultThreshold: 80.0, Packages: "...", Exclude: "..."}`. + +Cover these scenarios — each is one subtest in the table: + +1. **No exclude → existing behavior unchanged.** `Exclude: ""`, three packages `pkg/a`, `pkg/b`, `pkg/c` from `goListPackages("./...")`. Assert: all three appear in output; "Excluded" notice does **not** appear; `err == nil`. + +2. **Subset exclusion.** `Exclude: "./pkg/b"`, three packages from `./...`, exclude pattern returns `["example.com/mod/pkg/b"]`. Assert: `pkg/a` and `pkg/c` appear; `pkg/b` does **not** appear in output; "Excluded 1 package(s)" notice appears; `err == nil`. + +3. **No-op exclude pattern.** `Exclude: "./does-not-exist/..."`, exclude pattern returns `[]`. Assert: all three packages appear; "Excluded" notice does **not** appear (exclusion removed nothing); `err == nil`. + +4. **Multiple patterns with whitespace.** `Exclude: "./pkg/a, ./pkg/c"` (note the space after the comma). Exclude lookup must trim whitespace. Assert: only `pkg/b` appears; "Excluded 2 package(s)" notice appears; `err == nil`. + +5. **Exclude-all.** `Exclude: "./..."` returns the full three-package list. Assert: no per-package output rows; "Excluded 3 package(s)" notice appears; output contains `All packages meet minimum coverage requirement` and `Total time:`; `err == nil`. + +Also add one error-path test, `TestRun_Exclude_GoListError`, modeled on `TestRun_GoListError`: + +- `goListPackages` returns the package list correctly for `Packages` but returns `error` for the exclude pattern. +- Assert: `err != nil`; error message contains the pattern string and the wrapped underlying error message (sanity-check the `fmt.Errorf("resolve exclude %q: %w", ...)` format). + +Use `//nolint:exhaustruct` on `runner` and `Opts` literals to match the existing test style. + +### Milestone 4 — Validation pass + +Run preflight from the repo root and fix any findings. The plan considers the work complete only when preflight reports clean (see Validation and Acceptance below for the exact expectation). + +## Concrete Steps + +All commands are run from `/home/ben/miru/workbench2/repos/gotools/` unless stated otherwise. The current branch is `feat/covgate-exclude-flag`; do not switch branches. + +### Step 0 — Sanity baseline + +From `gotools/`: + + git status + +Expected: `On branch feat/covgate-exclude-flag` and a clean working tree (or only this plan file as a tracked addition). + + go build ./... + +Expected: completes silently with exit 0. + + go test ./internal/services/covgate/... ./internal/commands/... + +Expected: `ok` for both packages. + +### Step 1 — Implement Milestone 1 (service) + +Edit `internal/services/covgate/covgate.go` per "Plan of Work — Milestone 1" above. + +Sanity check the build: + + go build ./internal/services/covgate/... + +Expected: exit 0, no output. + +Commit: + + git add internal/services/covgate/covgate.go + git commit -m "feat(covgate): support --exclude to skip packages from coverage measurement" + +### Step 2 — Implement Milestone 2 (CLI + cmd tests) + +Edit `internal/commands/covgate.go` and `internal/commands/commands_test.go` per "Plan of Work — Milestone 2". + +Run: + + go build ./... + go test ./internal/commands/... + +Expected: both pass. The new flag tests should pass against the wired-up flag. + +Commit: + + git add internal/commands/covgate.go internal/commands/commands_test.go + git commit -m "feat(cli): expose covgate --exclude flag" + +### Step 3 — Implement Milestone 3 (service tests) + +Edit `internal/services/covgate/covgate_test.go` per "Plan of Work — Milestone 3". + +Run: + + go test ./internal/services/covgate/... + +Expected output (line counts approximate): + + ok github.com/mirurobotics/gotools/internal/services/covgate 0.5s + +If any subtest fails, inspect the buffer assertions; the most common pitfall is forgetting that the "Excluded" notice should *not* appear when zero packages were removed. + +Commit: + + git add internal/services/covgate/covgate_test.go + git commit -m "test(covgate): cover --exclude semantics across no-op, subset, multi, and full removal" + +### Step 4 — Milestone 4 preflight + +From `gotools/`: + + ./scripts/preflight.sh + +Expected last line: + + === All checks passed === + +and exit code 0. + +If preflight is not clean: + +- Read the offending output (lint, covgate, or surface lint). +- Apply fixes in place. Common items: gofumpt formatting, golangci-lint findings on the new test file, any new exported symbol catching surface-lint attention. +- Rerun preflight. Repeat until clean. + +Commit any fixes: + + git add -A + git commit -m "chore(covgate): preflight fixes" + +Skip this commit if preflight was clean on the first run. + +## Validation and Acceptance + +Acceptance is observable behavior plus a clean preflight, in this order. **Changes MUST NOT be published (no push, no PR) until preflight reports clean.** + +1. **Service tests pass.** From `gotools/`: + + go test ./internal/services/covgate/... + + Expected: `ok` line; the new tests `TestRun_Exclude_NoExclude`, `TestRun_Exclude_Subset`, `TestRun_Exclude_NoOpPattern`, `TestRun_Exclude_MultiplePatternsWithWhitespace`, `TestRun_Exclude_AllPackages`, and `TestRun_Exclude_GoListError` all pass. Before this change those tests do not exist; after, they pass. + +2. **Command tests pass.** + + go test ./internal/commands/... + + Expected: `ok`. The updated `TestNewCovgateCommand_Flags` and `TestNewCovgateCommand_FlagDefaults` see the new `exclude` flag with default `""`. + +3. **Build is clean.** + + go build ./... + + Expected: exit 0, no output. + +4. **Preflight is clean — mandatory gate.** From `gotools/`: + + ./scripts/preflight.sh + + Expected final line: `=== All checks passed ===` with exit 0. **No changes are published, no PR is opened, and no branch is pushed until this command reports clean.** If it fails on any of lint, covgate, or surface lint, fix the underlying issue and rerun. Do not bypass preflight. + +5. **End-to-end smoke (manual).** Build the `miru` binary and run against the gotools repo itself, demonstrating the new flag: + + go build -o /tmp/miru ./cmd/miru + /tmp/miru covgate --packages ./... --exclude ./internal/services/gocover/... + + Expected: the output contains the line `Excluded N package(s) from coverage measurement` for some `N >= 1`, and no row mentions a `gocover` package. Without `--exclude`, those packages appear normally. (This step is for manual verification only — not part of automated tests.) + +## Idempotence and Recovery + +- Service and CLI edits (Milestones 1–2): purely additive. Re-running the edits is safe; if a step is partially applied, re-read the file and reconcile against the "Plan of Work" instructions. +- Test additions (Milestone 3): if Go reports duplicate function names after a retry, remove the half-applied block and re-paste from this plan. +- Preflight (Milestone 4): idempotent — re-running `./scripts/preflight.sh` repeatedly is safe and reflects the current working tree. +- Commits: each milestone has its own commit (see Concrete Steps). If a commit needs to be redone, prefer adding a new commit over `git commit --amend` so the milestone boundary stays visible in PR review. To discard uncommitted changes for a fresh retry, use `git restore ` on specific files — do **not** use `git reset --hard` because it discards work in unrelated files. +- Rollback path: revert the milestone commits in reverse order with `git revert ` (per-commit) if the change must be removed after merge. The change is purely additive (new flag, new tests, no semantic change when flag is unset), so reverting is low-risk. From 8e6860f1fdc616b8c71c5f4c82fb56b336bed075 Mon Sep 17 00:00:00 2001 From: Benjamin Smidt Date: Fri, 15 May 2026 09:58:27 -0700 Subject: [PATCH 2/6] feat(covgate): support --exclude to skip packages from coverage measurement --- internal/services/covgate/covgate.go | 55 +++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/internal/services/covgate/covgate.go b/internal/services/covgate/covgate.go index 2e90780..dfc2d9c 100644 --- a/internal/services/covgate/covgate.go +++ b/internal/services/covgate/covgate.go @@ -14,7 +14,11 @@ import ( // Opts holds the options for the covgate service. type Opts struct { - Packages string + Packages string + // Exclude is a comma-separated list of Go list patterns whose + // matched import paths are removed from the measurement set + // before tests run. Empty means no exclusion. + Exclude string SrcPrefix string TestDir string DefaultThreshold float64 @@ -90,6 +94,11 @@ func (r *runner) run(opts Opts) error { return err } + pkgs, err = r.applyExclude(pkgs, opts.Exclude, w) + if err != nil { + return err + } + printHeader(w) ctx := checkPackageCtx{ @@ -107,6 +116,50 @@ func (r *runner) run(opts Opts) error { return r.printResults(w, results, wallTime) } +// applyExclude removes packages matched by the comma-separated +// exclude patterns from pkgs. It preserves the original order of +// pkgs and prints a one-line notice to w when any package is +// actually removed. An empty (or whitespace-only) exclude string +// returns pkgs unchanged with no output. +func (r *runner) applyExclude( + pkgs []string, exclude string, w io.Writer, +) ([]string, error) { + if strings.TrimSpace(exclude) == "" { + return pkgs, nil + } + + excluded := make(map[string]struct{}) + for _, raw := range strings.Split(exclude, ",") { + entry := strings.TrimSpace(raw) + if entry == "" { + continue + } + matched, err := r.goListPackages(entry) + if err != nil { + return nil, fmt.Errorf("resolve exclude %q: %w", entry, err) + } + for _, p := range matched { + excluded[p] = struct{}{} + } + } + + kept := make([]string, 0, len(pkgs)) + for _, p := range pkgs { + if _, drop := excluded[p]; drop { + continue + } + kept = append(kept, p) + } + + if removed := len(pkgs) - len(kept); removed > 0 { + _, _ = fmt.Fprintf( + w, "Excluded %d package(s) from coverage measurement\n", + removed, + ) + } + return kept, nil +} + func (r *runner) runPackages( pkgs []string, ctx checkPackageCtx, parallelism int, ) []checkResult { From 2e5ae282581d682e7f8a8910d7fa6db4ca87ce44 Mon Sep 17 00:00:00 2001 From: Benjamin Smidt Date: Fri, 15 May 2026 09:58:51 -0700 Subject: [PATCH 3/6] feat(cli): expose covgate --exclude flag --- internal/commands/commands_test.go | 2 ++ internal/commands/covgate.go | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/internal/commands/commands_test.go b/internal/commands/commands_test.go index b4d485c..5a325e8 100644 --- a/internal/commands/commands_test.go +++ b/internal/commands/commands_test.go @@ -341,6 +341,7 @@ func TestNewCovgateCommand_Flags(t *testing.T) { wantType string }{ {"packages", "string"}, + {"exclude", "string"}, {"src-prefix", "string"}, {"test-dir", "string"}, {"default-threshold", "float64"}, @@ -365,6 +366,7 @@ func TestNewCovgateCommand_FlagDefaults(t *testing.T) { stringDefaults := map[string]string{ "packages": "./...", + "exclude": "", "src-prefix": "pkg", "test-dir": "", } diff --git a/internal/commands/covgate.go b/internal/commands/covgate.go index 34b8266..a539af7 100644 --- a/internal/commands/covgate.go +++ b/internal/commands/covgate.go @@ -28,6 +28,10 @@ func NewCovgateCommand() *cobra.Command { fl := cmd.Flags() fl.StringVar(&opts.Packages, "packages", "./...", "Go package pattern for go list") + fl.StringVar( + &opts.Exclude, "exclude", "", + "comma-separated list of Go list patterns to exclude from coverage measurement", + ) fl.StringVar( &opts.SrcPrefix, "src-prefix", "pkg", "source prefix for mapping external test dirs", From 29fdaa7cb844873280e2ae05b47fb5e1d09b9639 Mon Sep 17 00:00:00 2001 From: Benjamin Smidt Date: Fri, 15 May 2026 09:59:29 -0700 Subject: [PATCH 4/6] test(covgate): cover --exclude semantics across no-op, subset, multi, and full removal --- internal/services/covgate/covgate_test.go | 156 ++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/internal/services/covgate/covgate_test.go b/internal/services/covgate/covgate_test.go index 4d53aec..28a7bde 100644 --- a/internal/services/covgate/covgate_test.go +++ b/internal/services/covgate/covgate_test.go @@ -164,6 +164,162 @@ func TestRun_WithFailure(t *testing.T) { } } +func TestRun_Exclude(t *testing.T) { + const ( + pkgA = "example.com/mod/pkg/a" + pkgB = "example.com/mod/pkg/b" + pkgC = "example.com/mod/pkg/c" + ) + allThree := []string{pkgA, pkgB, pkgC} + + cases := []struct { + name string + exclude string + lookup map[string][]string + wantContains []string + wantNotContain []string + }{ + { + name: "NoExclude", + exclude: "", + lookup: map[string][]string{ + "./...": allThree, + }, + wantContains: []string{"pkg/a", "pkg/b", "pkg/c"}, + wantNotContain: []string{"Excluded"}, + }, + { + name: "Subset", + exclude: "./pkg/b", + lookup: map[string][]string{ + "./...": allThree, + "./pkg/b": {pkgB}, + }, + wantContains: []string{ + "pkg/a", + "pkg/c", + "Excluded 1 package(s) from coverage measurement", + }, + wantNotContain: []string{"pkg/b"}, + }, + { + name: "NoOpPattern", + exclude: "./does-not-exist/...", + lookup: map[string][]string{ + "./...": allThree, + "./does-not-exist/...": {}, + }, + wantContains: []string{"pkg/a", "pkg/b", "pkg/c"}, + wantNotContain: []string{"Excluded"}, + }, + { + name: "MultiplePatternsWithWhitespace", + exclude: "./pkg/a, ./pkg/c", + lookup: map[string][]string{ + "./...": allThree, + "./pkg/a": {pkgA}, + "./pkg/c": {pkgC}, + }, + wantContains: []string{ + "pkg/b", + "Excluded 2 package(s) from coverage measurement", + }, + wantNotContain: []string{"pkg/a", "pkg/c"}, + }, + { + name: "AllPackages", + exclude: "./...", + lookup: map[string][]string{ + "./...": allThree, + }, + wantContains: []string{ + "Excluded 3 package(s) from coverage measurement", + "All packages meet minimum coverage requirement", + "Total time:", + }, + wantNotContain: []string{"pkg/a", "pkg/b", "pkg/c"}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + lookup := tc.lookup + var buf bytes.Buffer + //nolint:exhaustruct // test uses partial initialization + r := runner{ + goModule: func() (string, error) { return modName, nil }, + goListPackages: func(pattern string) ([]string, error) { + out, ok := lookup[pattern] + if !ok { + return nil, fmt.Errorf("unexpected pattern %q", pattern) + } + return out, nil + }, + measure: fakeMeasure(90.0), + } + + //nolint:exhaustruct // test uses partial initialization + err := r.run(Opts{ + Out: &buf, + DefaultThreshold: 80.0, + Packages: "./...", + Exclude: tc.exclude, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + out := buf.String() + for _, want := range tc.wantContains { + if !strings.Contains(out, want) { + t.Errorf("output missing %q:\n%s", want, out) + } + } + for _, unwanted := range tc.wantNotContain { + if strings.Contains(out, unwanted) { + t.Errorf("output unexpectedly contains %q:\n%s", unwanted, out) + } + } + }) + } +} + +func TestRun_Exclude_GoListError(t *testing.T) { + var buf bytes.Buffer + //nolint:exhaustruct // test uses partial initialization + r := runner{ + goModule: func() (string, error) { return modName, nil }, + goListPackages: func(pattern string) ([]string, error) { + if pattern == "./..." { + return []string{"example.com/mod/pkg/a"}, nil + } + return nil, fmt.Errorf("list failed") + }, + measure: fakeMeasure(90.0), + } + + //nolint:exhaustruct // test uses partial initialization + err := r.run(Opts{ + Out: &buf, + DefaultThreshold: 80.0, + Packages: "./...", + Exclude: "./pkg/bogus", + }) + if err == nil { + t.Fatal("expected error from exclude pattern resolution") + } + msg := err.Error() + if !strings.Contains(msg, "./pkg/bogus") { + t.Errorf("error missing pattern %q: %v", "./pkg/bogus", err) + } + if !strings.Contains(msg, "list failed") { + t.Errorf("error missing wrapped cause %q: %v", "list failed", err) + } + if !strings.Contains(msg, "resolve exclude") { + t.Errorf("error missing context prefix %q: %v", "resolve exclude", err) + } +} + func TestRun_Parallelism(t *testing.T) { // Use a single temp dir so all three packages share the same cwd. tmp := t.TempDir() From 9bf0d5b54a364e3136fa3779a5e34eadabf746d5 Mon Sep 17 00:00:00 2001 From: Benjamin Smidt Date: Fri, 15 May 2026 10:00:52 -0700 Subject: [PATCH 5/6] chore(covgate): preflight fixes --- internal/services/covgate/covgate_test.go | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/internal/services/covgate/covgate_test.go b/internal/services/covgate/covgate_test.go index 28a7bde..ad08bfd 100644 --- a/internal/services/covgate/covgate_test.go +++ b/internal/services/covgate/covgate_test.go @@ -180,21 +180,16 @@ func TestRun_Exclude(t *testing.T) { wantNotContain []string }{ { - name: "NoExclude", - exclude: "", - lookup: map[string][]string{ - "./...": allThree, - }, + name: "NoExclude", + exclude: "", + lookup: map[string][]string{"./...": allThree}, wantContains: []string{"pkg/a", "pkg/b", "pkg/c"}, wantNotContain: []string{"Excluded"}, }, { name: "Subset", exclude: "./pkg/b", - lookup: map[string][]string{ - "./...": allThree, - "./pkg/b": {pkgB}, - }, + lookup: map[string][]string{"./...": allThree, "./pkg/b": {pkgB}}, wantContains: []string{ "pkg/a", "pkg/c", @@ -213,8 +208,11 @@ func TestRun_Exclude(t *testing.T) { wantNotContain: []string{"Excluded"}, }, { + // Includes empty entries before, between, and after + // non-empty ones so the trim/skip-empty path is + // exercised end-to-end. name: "MultiplePatternsWithWhitespace", - exclude: "./pkg/a, ./pkg/c", + exclude: ", ./pkg/a, , ./pkg/c,", lookup: map[string][]string{ "./...": allThree, "./pkg/a": {pkgA}, @@ -229,9 +227,7 @@ func TestRun_Exclude(t *testing.T) { { name: "AllPackages", exclude: "./...", - lookup: map[string][]string{ - "./...": allThree, - }, + lookup: map[string][]string{"./...": allThree}, wantContains: []string{ "Excluded 3 package(s) from coverage measurement", "All packages meet minimum coverage requirement", From 6ff782caa53426fed6aaa930abb1534da6e9d28d Mon Sep 17 00:00:00 2001 From: Benjamin Smidt Date: Fri, 15 May 2026 10:01:40 -0700 Subject: [PATCH 6/6] docs(plans): mark covgate --exclude flag complete Co-Authored-By: Claude Opus 4.7 --- plans/{active => completed}/20260515-covgate-exclude-flag.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename plans/{active => completed}/20260515-covgate-exclude-flag.md (100%) diff --git a/plans/active/20260515-covgate-exclude-flag.md b/plans/completed/20260515-covgate-exclude-flag.md similarity index 100% rename from plans/active/20260515-covgate-exclude-flag.md rename to plans/completed/20260515-covgate-exclude-flag.md