You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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:
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/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.
Summary
Codecov reports 73.76% line coverage on
main(13332 lines, 9834 hits, 2693 misses, 805 partials). The project target — set incodecov.ymland 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 localcoverage.outprofile, so the work list is concrete rather than exploratory.Problem
informational: truetoday 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%.Context
Current state, measured 2026-08-09:
main): 73.76% — 13332 lines, 9834 hits, 2693 misses, 805 partials. Lines needed as hits for 80.0%: 832.task coverage): 77.33% — 9602 statements, 7425 covered. The ratchet inscripts/coverage-thresholdis at77.3.codecov.ymland 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/testutiland non-Go directories are excluded from Codecov; see theignore:block incodecov.yml.internal/testutilalready providesFakeRunner(a scriptedenvironment.Runneranswering commands from a fixture map, plusRunStreaming), 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:internal/app/app.gointernal/runner/session.gointernal/telemetry/telemetry.gointernal/sandbox/sandbox.gointernal/github/github.gointernal/skill/skill.gointernal/service/service.gointernal/app/status_tui.gointernal/fork/fork.gointernal/logging/rotate.gointernal/sandbox/proxy/proxy.gointernal/sandbox/container/container.gointernal/state/state.gointernal/provider/codex.gointernal/provider/gemini.gointernal/app/report.gointernal/hardening/hardening.gocmd/gh-sandbox/main.gointernal/github/sanitize.gointernal/worktree/worktree.gointernal/provider/compatibility.gointernal/app/logs.gointernal/github/comment_format.gocmd/vigilante/main.gointernal/logging/logging.goReproduce the per-file table with:
curl -s "https://api.codecov.io/api/v2/github/aliengiraffe/repos/vigilante/totals/?branch=main"Desired Outcome
mainreaches 80.0% or higher.scripts/coverage-thresholdis raised to the new measured local statement total in the same pull request (the ratchet only moves up).Non-goals (explicitly out of scope):
codecov.yml'sproject.default.informationaltofalsein this issue. Whether to hard-gate is a separate decision once the number is above target.ignore:list incodecov.yml, lower a target, or otherwise move the number by changing what is measured. The gain must come from tests.io.Writerinstead of writing toos.Stdoutdirectly) are acceptable where they are the only way to test a unit; broad restructuring is not.internal/app/app.go. It is a 5486-line file and splitting it is its own issue.testing,httptest,t.TempDir,t.Setenv) plus the existinginternal/testutilfakes.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) andinternal/provider/gemini.go(52) — theBuild*Invocationmethods are pure constructors returning anInvocation{Name, Args}.DisplayName,RequiredTools, andEnsureRuntimeInstalledare equally trivial.internal/provider/claude.gois already at ~80% andinternal/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) —ValidateRuntimeCompatibilityis at 0%; it callsrunner.Run(ctx, "", tool, "--version")and delegates to the already-testedValidateVersionOutput.testutil.FakeRunnercovers both the success and thedetect %s CLI versionerror path. Also coverdescribeCompatibilityfor an unknown provider ID.internal/github/sanitize.go(41) — partial branches insanitizeBodyFlags(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,fallbackCommentValueare all 66–75%. Cover the boundary cases (zero, negative, over-100, empty).internal/logging/logging.go(16, 0%) —NewDaemonLoggeragainstt.TempDir()(assert the file is created and a logged record lands in it with an RFC3339 local timestamp), plus the four one-linediscardHandlermethods viaDiscard().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 againsthttptest.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 att.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.gopure helpers (~80) — do not attempt the whole file. Target the self-contained functions:dispatchFailureNextStep(44.4%) andresumeFailureNextStep(40%) are pure switches overstate.Sessionproducing operator-facing strings, with one test case per branch;dispatchFailureFingerprintandresumeFailureFingerprintare pure joins;stalledSessionThreshold(42.9%) readsVIGILANTE_STALLED_SESSION_THRESHOLDand is coverable witht.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,Appendare 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,isRepositoryNotFoundat 0%,ensureForkRepositoryat 20.8%. All of it shells out togh, sotestutil.FakeRunnerscripts 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 toVIGILANTE_PROXY_URL/api/sandbox/gh, and returns an exit code.httptest.NewServerplust.Setenvcovers the missing-URL, missing-token, empty-command, non-JSON-response, and success paths. It currently writes toos.Stdout/os.Stderrdirectly — changingrun()to take(stdout, stderr io.Writer)withmain()passing the real ones is an acceptable minimal seam.main()itself callingos.Exitmay stay uncovered.internal/sandbox/proxy/proxy.go(59, 52.80%) —Start,Stop,Addr,DeregisterSession,handleTokenRefreshat 0%. Start the proxy on127.0.0.1:0in-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/resolveRunnerat 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
mainshows >= 80.0% coverage after the work lands, verifiable viacurl -s "https://api.codecov.io/api/v2/github/aliengiraffe/repos/vigilante/totals/?branch=main".task coveragepasses and reports a total at least 4 percentage points above the current 77.33% statement baseline.scripts/coverage-thresholdis updated to the new measured total (rounded down to one decimal, as today) andtask coveragepasses against it.task testpasses with no skipped or flaky tests introduced; the suite remains deterministic (no reliance on wall-clock timing, network access, or an installedgh/docker/provider CLI).golangci-lint runpasses on the new test files.codecov.ymlis unchanged apart from comment text; specifically, theignore:list is not expanded, no target is lowered, andproject.default.informationalis stilltrue.internal/provider(claude,codex,gemini,opencode) has itsBuild*Invocationmethods asserted for bothNameand the fullArgsslice.internal/logging/logging.goandcmd/gh-sandbox/main.goare no longer at 0% coverage.CONTRIBUTING.md("about 67% on Codecov and about 71% locally") are updated to the post-change measured values.Testing Expectations
_test.gofiles (internal/provider/provider_test.go,internal/logging/rotate_test.go) over new parallel files where a suitable one already exists.internal/testutil.FakeRunnerfor anything that shells out togit,gh,docker, or a provider CLI. Do not invoke real binaries.t.TempDir()for filesystem work andt.Setenvfor environment-dependent helpers so tests do not leak state between packages.httptest.NewServerforcmd/gh-sandbox,internal/telemetry's HTTP exporter, andinternal/sandbox/proxy. Bind to127.0.0.1:0; never a fixed port.err == nilmoves the number without protecting anything and should not be written. Each test case must assert a concrete value, string, or error condition.--dangerously-bypass-approvals-and-sandboxand the worktree path are passed through). Assert the exactArgsslice so an accidental flag change fails a test.internal/github/sanitize.gostrips secrets and body flags from logged commands. Cover the paths that redact, not just the pass-through ones.internal/provider/compatibility.gogates on CLI version ranges; cover both boundaries (min inclusive, max exclusive) for at least one provider.internal/logging/rotate.gobounds disk usage; assert that appending past the size limit actually rotates and that the old file is retained/pruned per the configured limits.go test ./internal/provider/...), thentask testandtask coveragebefore opening the pull request.Operational / UX Considerations
codecov.ymlsetsafter_n_builds: 1, so no workflow changes are needed.patchstatus 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.scripts/coverage-thresholdis a ratchet: once raised it must not be lowered, so raise it to the measured value rather than an aspirational one.codecov.yml,scripts/coverage-threshold, andCONTRIBUTING.mdaccurate. They exist because the local and Codecov percentages legitimately disagree, and a future contributor reading a stale number will draw the wrong conclusion.