Skip to content

Raise Codecov coverage to 80% by covering the highest-density easy wins #505

Description

@nicobistolfi

Summary

Codecov reports 73.76% line coverage on main (13332 lines, 9834 hits, 2693 misses, 805 partials). The project target — set in codecov.yml and CONTRIBUTING.md, and required by awesome-go's quality standard — is 80%. Closing that gap means turning roughly 832 more lines into hits. This issue asks for that gap to be closed by writing tests against the cheapest, highest-density targets first: pure functions and small deterministic helpers that need no new test infrastructure. The measurements below were taken from the live Codecov API and from a local coverage.out profile, so the work list is concrete rather than exploratory.

Problem

  • Codecov's project status is informational: true today precisely because a hard 80% gate would fail every pull request. The repo cannot enforce its own stated standard until the total actually reaches 80%.
  • The current backfill strategy is "cover what you touch" (Codecov patch status at 80%). That holds the line but does not close a ~832-line gap in any reasonable timeframe.
  • Coverage is concentrated in a small number of files with a lot of easily-testable pure logic left untested. The gap is not hard to close; it just has not been targeted deliberately.

Context

Current state, measured 2026-08-09:

  • Codecov (line coverage, main): 73.76% — 13332 lines, 9834 hits, 2693 misses, 805 partials. Lines needed as hits for 80.0%: 832.
  • Local (statement coverage, task coverage): 77.33% — 9602 statements, 7425 covered. The ratchet in scripts/coverage-threshold is at 77.3.
  • The two numbers differ by methodology, not by a coverage change; codecov.yml and CONTRIBUTING.md already explain this. Note that the numbers quoted in CONTRIBUTING.md ("about 67% on Codecov and about 71% locally") are now stale and should be refreshed as part of this work.
  • internal/testutil and non-Go directories are excluded from Codecov; see the ignore: block in codecov.yml.
  • internal/testutil already provides FakeRunner (a scripted environment.Runner answering commands from a fixture map, plus RunStreaming), which is the standard way to test code that shells out.

Where the uncovered lines actually are

Codecov uncovered lines (misses + partials) per file, worst first:

File Uncovered Total Coverage
internal/app/app.go 1715 5486 68.73%
internal/runner/session.go 152 525 71.04%
internal/telemetry/telemetry.go 150 622 75.88%
internal/sandbox/sandbox.go 120 263 54.37%
internal/github/github.go 117 623 81.21%
internal/skill/skill.go 116 633 81.67%
internal/service/service.go 101 334 69.76%
internal/app/status_tui.go 74 317 76.65%
internal/fork/fork.go 70 109 35.77%
internal/logging/rotate.go 68 254 73.22%
internal/sandbox/proxy/proxy.go 59 125 52.80%
internal/sandbox/container/container.go 55 96 42.70%
internal/state/state.go 54 278 80.57%
internal/provider/codex.go 54 68 20.58%
internal/provider/gemini.go 52 68 23.52%
internal/app/report.go 52 214 75.70%
internal/hardening/hardening.go 52 232 77.58%
cmd/gh-sandbox/main.go 47 47 0.00%
internal/github/sanitize.go 41 165 75.15%
internal/worktree/worktree.go 39 174 77.58%
internal/provider/compatibility.go 34 94 63.82%
internal/app/logs.go 33 156 78.84%
internal/github/comment_format.go 24 108 77.77%
cmd/vigilante/main.go 18 35 48.57%
internal/logging/logging.go 16 16 0.00%

Reproduce the per-file table with:

curl -s "https://api.codecov.io/api/v2/github/aliengiraffe/repos/vigilante/totals/?branch=main"

Desired Outcome

  • Codecov project coverage on main reaches 80.0% or higher.
  • scripts/coverage-threshold is raised to the new measured local statement total in the same pull request (the ratchet only moves up).
  • The new tests assert real behavior — inputs, outputs, and error paths — not merely that a function can be called.

Non-goals (explicitly out of scope):

  • Do not flip codecov.yml's project.default.informational to false in this issue. Whether to hard-gate is a separate decision once the number is above target.
  • Do not expand the ignore: list in codecov.yml, lower a target, or otherwise move the number by changing what is measured. The gain must come from tests.
  • Do not refactor production code for its own sake. Small, behavior-preserving seams (e.g. accepting an io.Writer instead of writing to os.Stdout directly) are acceptable where they are the only way to test a unit; broad restructuring is not.
  • Do not rewrite internal/app/app.go. It is a 5486-line file and splitting it is its own issue.
  • No new third-party test dependencies. Standard library (testing, httptest, t.TempDir, t.Setenv) plus the existing internal/testutil fakes.

Implementation Notes

Work the tiers in order. Tiers 1–3 total roughly 870 uncovered lines against a required 832, so expect to need essentially all of tier 1 and 2 and most of tier 3; tier 4 is the buffer if realized gains fall short (not every uncovered line is reachable).

Tier 1 — pure functions, no fakes needed (~221 lines)

Highest density, lowest risk. Start here.

  • internal/provider/codex.go (54) and internal/provider/gemini.go (52) — the Build*Invocation methods are pure constructors returning an Invocation{Name, Args}. DisplayName, RequiredTools, and EnsureRuntimeInstalled are equally trivial. internal/provider/claude.go is already at ~80% and internal/provider/provider_test.go (569 lines) has the pattern to mirror — extend it to table-drive all four providers rather than duplicating per-file tests.
  • internal/provider/compatibility.go (34)ValidateRuntimeCompatibility is at 0%; it calls runner.Run(ctx, "", tool, "--version") and delegates to the already-tested ValidateVersionOutput. testutil.FakeRunner covers both the success and the detect %s CLI version error path. Also cover describeCompatibility for an unknown provider ID.
  • internal/github/sanitize.go (41) — partial branches in sanitizeBodyFlags (65.6%), sanitizeFileOrStdinValue (53.8%), isGitHubBodyCommand / isGitHubAPIBodyCommand (75%), proxyFlagNeedsValue (71.4%). Pure string handling; table tests.
  • internal/github/comment_format.go (24)formatMinutes, formatAbsoluteTime, minutesUntil, clampPercent, usedRequests, fallbackCommentValue are all 66–75%. Cover the boundary cases (zero, negative, over-100, empty).
  • internal/logging/logging.go (16, 0%)NewDaemonLogger against t.TempDir() (assert the file is created and a logged record lands in it with an RFC3339 local timestamp), plus the four one-line discardHandler methods via Discard().

Tier 2 — pure helpers inside larger files (~405 lines)

  • internal/telemetry/telemetry.go (150) — the uncovered surface is almost entirely pure classifiers and argument parsers with partial branch coverage: commandFeatureArea (40%), internalCommandFeatureArea (50%), internalFlagTakesValue (60%), proxyFlagTakesValue (60%), internalCommandTokenAllowed (62.5%), downstreamServiceCategory (66.7%), boundedOperation (66.7%), nextProxySubcommand (70%), classifyDownstreamRateLimit (71.4%), proxyCommandPath / firstProxyCommandToken (75%), Export (64%). newHTTPExporter (0%) is testable against httptest.NewServer.
  • internal/service/service.go (101)normalizeDaemonVersionOutput (40%), parseSystemdExecutable (57.1%), shellDerivedPath (71.4%), daemonExecutableFromServiceDefinition (71.4%), runtimeTool (75%) are pure. installLaunchdService / installSystemdUserService (0%) write plist/unit files — testable by pointing them at t.TempDir() if a path seam already exists; skip them if it does not, rather than restructuring installation.
  • internal/app/status_tui.go (74)pageSize (0%), statusDaemonVersion (40%), statusRateLimitStyle (50%), statusStateStyle (75%), statusDashboard (50%) are rendering/selection helpers over plain values. Assert on the returned strings/styles; do not attempt to drive the Bubble Tea event loop.
  • internal/app/app.go pure helpers (~80) — do not attempt the whole file. Target the self-contained functions: dispatchFailureNextStep (44.4%) and resumeFailureNextStep (40%) are pure switches over state.Session producing operator-facing strings, with one test case per branch; dispatchFailureFingerprint and resumeFailureFingerprint are pure joins; stalledSessionThreshold (42.9%) reads VIGILANTE_STALLED_SESSION_THRESHOLD and is coverable with t.Setenv (unset, valid, invalid, non-positive); ExpandPath (30%) covers empty input, ~, ~/x, and a relative path; runCompletionCommand (47.4%) covers each supported shell plus the unsupported-shell error.

Tier 3 — needs a fake, temp dir, or local HTTP server (~244 lines)

  • internal/logging/rotate.go (68)Configure, Path, SetLimits, Append are at 0%. rotate_test.go (497 lines) already exists and uses temp dirs; extend it. Drive an actual rotation by setting a small size limit and appending past it.
  • internal/fork/fork.go (70, 35.77%)AuthenticatedOwner, EnsureFork, isRepositoryNotFound at 0%, ensureForkRepository at 20.8%. All of it shells out to gh, so testutil.FakeRunner scripts the responses. Cover the fork-exists, fork-created, and repository-not-found paths.
  • cmd/gh-sandbox/main.go (47, 0%)run() is a single function that reads two env vars, POSTs to VIGILANTE_PROXY_URL/api/sandbox/gh, and returns an exit code. httptest.NewServer plus t.Setenv covers the missing-URL, missing-token, empty-command, non-JSON-response, and success paths. It currently writes to os.Stdout/os.Stderr directly — changing run() to take (stdout, stderr io.Writer) with main() passing the real ones is an acceptable minimal seam. main() itself calling os.Exit may stay uncovered.
  • internal/sandbox/proxy/proxy.go (59, 52.80%)Start, Stop, Addr, DeregisterSession, handleTokenRefresh at 0%. Start the proxy on 127.0.0.1:0 in-test and exercise the handlers over the real listener.

Tier 4 — buffer, only if tiers 1–3 fall short

internal/state/state.go (54), internal/app/report.go (52, generateReport/resolveRunner at 0%), internal/sandbox/container/container.go (55, all Docker exec — needs a fake runner seam), internal/app/logs.go (33), internal/hardening/hardening.go (52).

Sequencing

Landing this as several smaller pull requests (one per tier, or one per package group) is preferable to one large one, provided the final state clears 80%. If split, only the last PR raises scripts/coverage-threshold.

Acceptance Criteria

  • The Codecov project report for main shows >= 80.0% coverage after the work lands, verifiable via curl -s "https://api.codecov.io/api/v2/github/aliengiraffe/repos/vigilante/totals/?branch=main".
  • task coverage passes and reports a total at least 4 percentage points above the current 77.33% statement baseline.
  • scripts/coverage-threshold is updated to the new measured total (rounded down to one decimal, as today) and task coverage passes against it.
  • task test passes with no skipped or flaky tests introduced; the suite remains deterministic (no reliance on wall-clock timing, network access, or an installed gh/docker/provider CLI).
  • golangci-lint run passes on the new test files.
  • codecov.yml is unchanged apart from comment text; specifically, the ignore: list is not expanded, no target is lowered, and project.default.informational is still true.
  • Every provider in internal/provider (claude, codex, gemini, opencode) has its Build*Invocation methods asserted for both Name and the full Args slice.
  • internal/logging/logging.go and cmd/gh-sandbox/main.go are no longer at 0% coverage.
  • The stale coverage figures in CONTRIBUTING.md ("about 67% on Codecov and about 71% locally") are updated to the post-change measured values.
  • No production behavior changes, except where a documented minimal test seam is introduced; each such seam is called out in the pull request description.

Testing Expectations

  • Unit tests are the primary layer. Prefer table-driven tests in the existing _test.go files (internal/provider/provider_test.go, internal/logging/rotate_test.go) over new parallel files where a suitable one already exists.
  • Use internal/testutil.FakeRunner for anything that shells out to git, gh, docker, or a provider CLI. Do not invoke real binaries.
  • Use t.TempDir() for filesystem work and t.Setenv for environment-dependent helpers so tests do not leak state between packages.
  • Use httptest.NewServer for cmd/gh-sandbox, internal/telemetry's HTTP exporter, and internal/sandbox/proxy. Bind to 127.0.0.1:0; never a fixed port.
  • Assert behavior, not invocation. A test that calls a function and only checks err == nil moves the number without protecting anything and should not be written. Each test case must assert a concrete value, string, or error condition.
  • Regressions to cover explicitly:
    • Provider invocation arguments are a security-relevant surface (--dangerously-bypass-approvals-and-sandbox and the worktree path are passed through). Assert the exact Args slice so an accidental flag change fails a test.
    • internal/github/sanitize.go strips secrets and body flags from logged commands. Cover the paths that redact, not just the pass-through ones.
    • internal/provider/compatibility.go gates on CLI version ranges; cover both boundaries (min inclusive, max exclusive) for at least one provider.
    • internal/logging/rotate.go bounds disk usage; assert that appending past the size limit actually rotates and that the old file is retained/pruned per the configured limits.
  • Run the narrowest useful command per package first (go test ./internal/provider/...), then task test and task coverage before opening the pull request.

Operational / UX Considerations

  • No user-facing behavior change is expected. Any test seam that touches production signatures must be behavior-preserving and noted in the PR body.
  • CI already uploads a single coverage report; codecov.yml sets after_n_builds: 1, so no workflow changes are needed.
  • Codecov's patch status holds new code to 80%. Test files are absent from the Go coverage profile and therefore do not count against patch coverage, but any production line touched by a test seam does — cover it.
  • Raising scripts/coverage-threshold is a ratchet: once raised it must not be lowered, so raise it to the measured value rather than an aspirational one.
  • Keep the explanatory comments in codecov.yml, scripts/coverage-threshold, and CONTRIBUTING.md accurate. They exist because the local and Codecov percentages legitimately disagree, and a future contributor reading a stale number will draw the wrong conclusion.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions